From 0878f5bfb43bdc52d72968cbd7bf343e2ec6460d Mon Sep 17 00:00:00 2001 From: marcin mikołajczak Date: Tue, 13 Aug 2024 19:39:26 +0200 Subject: Inform users that Smithereen public polls are public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/components/poll/poll.vue | 7 +++++++ src/i18n/en.json | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue index 580e5377..e12f3e61 100644 --- a/src/components/poll/poll.vue +++ b/src/components/poll/poll.vue @@ -76,6 +76,13 @@ > {{ $t('polls.vote') }} + + {{ $t('polls.non_anonymous') }} +  ·  +
@@ -16,16 +16,28 @@ export default { props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold', 'templateKey'], data () { return { + relativeTimeMs: 0, relativeTime: { key: 'time.now', num: 0 }, interval: null } }, computed: { - localeDateString () { - const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale) + shouldUseAbsoluteTimeFormat () { + if (!this.$store.getters.mergedConfig.useAbsoluteTimeFormat) { + return false + } + return DateUtils.durationStrToMs(this.$store.getters.mergedConfig.absoluteTimeFormatMinAge) <= this.relativeTimeMs + }, + browserLocale () { + return localeService.internalToBrowserLocale(this.$i18n.locale) + }, + timeAsDate () { return typeof this.time === 'string' - ? new Date(Date.parse(this.time)).toLocaleString(browserLocale) - : this.time.toLocaleString(browserLocale) + ? new Date(Date.parse(this.time)) + : this.time + }, + localeDateString () { + return this.timeAsDate.toLocaleString(this.browserLocale) }, relativeTimeString () { const timeString = this.$i18n.tc(this.relativeTime.key, this.relativeTime.num, [this.relativeTime.num]) @@ -35,6 +47,40 @@ export default { } return timeString + }, + absoluteTimeString () { + if (this.longFormat) { + return this.localeDateString + } + const now = new Date() + const formatter = (() => { + if (DateUtils.isSameDay(this.timeAsDate, now)) { + return new Intl.DateTimeFormat(this.browserLocale, { + minute: 'numeric', + hour: 'numeric' + }) + } else if (DateUtils.isSameMonth(this.timeAsDate, now)) { + return new Intl.DateTimeFormat(this.browserLocale, { + hour: 'numeric', + day: 'numeric' + }) + } else if (DateUtils.isSameYear(this.timeAsDate, now)) { + return new Intl.DateTimeFormat(this.browserLocale, { + month: 'short', + day: 'numeric' + }) + } else { + return new Intl.DateTimeFormat(this.browserLocale, { + year: 'numeric', + month: 'short' + }) + } + })() + + return formatter.format(this.timeAsDate) + }, + relativeOrAbsoluteTimeString () { + return this.shouldUseAbsoluteTimeFormat ? this.absoluteTimeString : this.relativeTimeString } }, watch: { @@ -54,6 +100,7 @@ export default { methods: { refreshRelativeTimeObject () { const nowThreshold = typeof this.nowThreshold === 'number' ? this.nowThreshold : 1 + this.relativeTimeMs = DateUtils.relativeTimeMs(this.time) this.relativeTime = this.longFormat ? DateUtils.relativeTime(this.time, nowThreshold) : DateUtils.relativeTimeShort(this.time, nowThreshold) diff --git a/src/i18n/en.json b/src/i18n/en.json index 3f7ea282..0ed481c0 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -506,6 +506,8 @@ "autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available", "emoji_reactions_on_timeline": "Show emoji reactions on timeline", "emoji_reactions_scale": "Reactions scale factor", + "absolute_time_format": "Use absolute time format", + "absolute_time_format_min_age": "Only use for time older than this amount of time", "export_theme": "Save preset", "filtering": "Filtering", "wordfilter": "Wordfilter", diff --git a/src/modules/config.js b/src/modules/config.js index cf84234a..835dcce4 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -180,7 +180,9 @@ export const defaultState = { autocompleteSelect: undefined, // instance default closingDrawerMarksAsSeen: undefined, // instance default unseenAtTop: undefined, // instance default - ignoreInactionableSeen: undefined // instance default + ignoreInactionableSeen: undefined, // instance default + useAbsoluteTimeFormat: undefined, // instance defualt + absoluteTimeFormatMinAge: undefined // instance default } // caching the instance default properties diff --git a/src/modules/instance.js b/src/modules/instance.js index 99b8b5d5..994f60a5 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -119,6 +119,8 @@ const defaultState = { closingDrawerMarksAsSeen: true, unseenAtTop: false, ignoreInactionableSeen: false, + useAbsoluteTimeFormat: false, + absoluteTimeFormatMinAge: '0d', // Nasty stuff customEmoji: [], diff --git a/src/services/date_utils/date_utils.js b/src/services/date_utils/date_utils.js index ed8e1417..69398c0c 100644 --- a/src/services/date_utils/date_utils.js +++ b/src/services/date_utils/date_utils.js @@ -6,10 +6,13 @@ export const WEEK = 7 * DAY export const MONTH = 30 * DAY export const YEAR = 365.25 * DAY -export const relativeTime = (date, nowThreshold = 1) => { +export const relativeTimeMs = (date) => { if (typeof date === 'string') date = Date.parse(date) + return Math.abs(Date.now() - date) +} +export const relativeTime = (date, nowThreshold = 1) => { const round = Date.now() > date ? Math.floor : Math.ceil - const d = Math.abs(Date.now() - date) + const d = relativeTimeMs(date) const r = { num: round(d / YEAR), key: 'time.unit.years' } if (d < nowThreshold * SECOND) { r.num = 0 @@ -57,3 +60,39 @@ export const secondsToUnit = (unit, amount) => { case 'days': return (1000 * amount) / DAY } } + +export const isSameYear = (a, b) => { + return a.getFullYear() === b.getFullYear() +} + +export const isSameMonth = (a, b) => { + return a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() +} + +export const isSameDay = (a, b) => { + return a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate() +} + +export const durationStrToMs = (str) => { + if (typeof str !== 'string') { + return 0 + } + + const unit = str.replace(/[0-9,.]+/, '') + const value = str.replace(/[^0-9,.]+/, '') + switch (unit) { + case 'd': + return value * DAY + case 'h': + return value * HOUR + case 'm': + return value * MINUTE + case 's': + return value * SECOND + default: + return 0 + } +} -- cgit v1.2.3-70-g09d2 From 74e5bb9104551bfd8105ef6780c40502e9087adc Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 19 Sep 2024 04:24:35 +0300 Subject: serializer working --- src/services/theme_data/iss_deserializer.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/services/theme_data/iss_deserializer.js (limited to 'src') diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js new file mode 100644 index 00000000..44a0fade --- /dev/null +++ b/src/services/theme_data/iss_deserializer.js @@ -0,0 +1,3 @@ +export const deserializer (string) { +let level = 0 +} -- cgit v1.2.3-70-g09d2 From 1794d52731607baa3f80ea6a5ab2dbac5bd747ef Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 19 Sep 2024 15:43:38 +0300 Subject: changelog --- changelog.d/browsers-support.change | 9 +++++ src/services/theme_data/iss_serializer.js | 62 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 changelog.d/browsers-support.change create mode 100644 src/services/theme_data/iss_serializer.js (limited to 'src') diff --git a/changelog.d/browsers-support.change b/changelog.d/browsers-support.change new file mode 100644 index 00000000..a62e5024 --- /dev/null +++ b/changelog.d/browsers-support.change @@ -0,0 +1,9 @@ +Updated our build system to support browsers: + Safari >= 15 + Firefox >= 115 + Android > 4 + no Opera Mini support + no IE support + no "dead" (unmaintained) browsers support + +This does not guarantee that browsers will or will not work. diff --git a/src/services/theme_data/iss_serializer.js b/src/services/theme_data/iss_serializer.js new file mode 100644 index 00000000..39151e41 --- /dev/null +++ b/src/services/theme_data/iss_serializer.js @@ -0,0 +1,62 @@ +import { unroll } from './iss_utils' + +const getCanonicState = (state) => { + if (state) { + return ['normal', ...state.filter(x => x !== 'normal')] + } else { + return ['normal'] + } +} + +const getCanonicRuleHeader = ({ + component, + variant = 'normal', + parent, + state +}) => ({ + component, + variant, + parent, + state: getCanonicState(state) +}) + +const prepareRule = (rule) => { + const { parent } = rule + const chain = [...unroll(parent), rule].map(getCanonicRuleHeader) + const header = chain.map(({ component, variant, state }) => [ + component, + variant === 'normal' ? '' : ('.' + variant), + state.filter(s => s !== 'normal').map(s => ':' + s).join('') + ].join('')).join(' ') + + console.log(header, rule.directives) + const content = Object.entries(rule.directives).map(([key, value]) => { + let realValue = value + + switch (key) { + case 'shadow': + realValue = realValue.map(v => `${v.inset ? 'inset ' : ''}${v.x} ${v.y} ${v.blur} ${v.spread} ${v.color} / ${v.alpha}`) + } + + if (Array.isArray(realValue)) { + realValue = realValue.join(', ') + } + + return ` ${key}: ${realValue};` + }).sort().join('\n') + + return [ + header, + content + ] +} + +export const serialize = (ruleset) => { + // Scrapped idea: automatically combine same-set directives + // problem: might violate the order rules + + return ruleset.filter(r => Object.keys(r.directives).length > 0).map(r => { + const [header, content] = prepareRule(r) + return `${header} {\n${content}\n}\n\n` + }) +} -- cgit v1.2.3-70-g09d2 From af3b2e3dc9043e66806be85869d867e6b0d23c3c Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 19 Sep 2024 20:37:14 +0300 Subject: temp --- src/services/theme_data/iss_deserializer.js | 44 +++++++++++++++++++- src/services/theme_data/theme_data_3.service.js | 30 +++++++++++++- .../services/theme_data/iss_deserializer.spec.js | 47 ++++++++++++++++++++++ 3 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 test/unit/specs/services/theme_data/iss_deserializer.spec.js (limited to 'src') diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js index 44a0fade..431e1b94 100644 --- a/src/services/theme_data/iss_deserializer.js +++ b/src/services/theme_data/iss_deserializer.js @@ -1,3 +1,43 @@ -export const deserializer (string) { -let level = 0 +// this works nearly the same as HTML tree converter +export const deserialize = (input) => { + const buffer = [] + let textBuffer = '' + + const getCurrentBuffer = () => { + let current = buffer[buffer.length - 1][1] + if (current == null) { + current = { name: null, content: [] } + } + buffer.push(current) + return current + } + + // Processes current line buffer, adds it to output buffer and clears line buffer + const flushText = (content) => { + if (textBuffer === '') return + if (content) { + getCurrentBuffer().content.push(textBuffer) + } else { + getCurrentBuffer().name = textBuffer + } + textBuffer = '' + } + + for (let i = 0; i < input.length; i++) { + const char = input[i] + + if (char === ';') { + flushText(true) + } else if (char === '{') { + flushText(false) + } else if (char === '}') { + buffer.push({ name: null, content: [] }) + textBuffer = '' + } else { + textBuffer += char + } + } + + flushText() + return buffer } diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index cf58da11..cac450a8 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -23,6 +23,9 @@ import { findRules } from './iss_utils.js' import { parseCssShadow } from './css_utils.js' +import { + serialize +} from './iss_serializer.js' // Ensuring the order of components const components = { @@ -504,9 +507,32 @@ export const init = ({ console.debug('Eager processing took ' + (t2 - t1) + ' ms') } + // optimization to traverse big-ass array only once instead of twice + const eager = [] + const lazy = [] + + result.forEach(x => { + if (typeof x === 'function') { + lazy.push(x) + } else { + eager.push(x) + } + }) + + const serializedData = serialize(eager) + const file = new File(serializedData, 'ruleset.piss') + const blobUrl = URL.createObjectURL(file) + const a = document.createElement('a') + a.href = blobUrl + a.download = 'ruleset.piss' + document.body.appendChild(a) + a.dispatchEvent(new MouseEvent('click')) + URL.revokeObjectURL(blobUrl) + document.body.removeChild(a) + return { - lazy: result.filter(x => typeof x === 'function'), - eager: result.filter(x => typeof x !== 'function'), + lazy, + eager, staticVars, engineChecksum } diff --git a/test/unit/specs/services/theme_data/iss_deserializer.spec.js b/test/unit/specs/services/theme_data/iss_deserializer.spec.js new file mode 100644 index 00000000..f1967144 --- /dev/null +++ b/test/unit/specs/services/theme_data/iss_deserializer.spec.js @@ -0,0 +1,47 @@ +import { deserialize } from 'src/services/theme_data/iss_deserializer.js' + +/* eslint-disable quotes */ +const testData = ``` + Root { + --accent: color | #e2b188; + --badgeNotification: color | #e15932; + --bg: color | #0f161e; + --cBlue: color | #81beea; + --cGreen: color | #5dc94a; + --cOrange: color | #ffc459; + --cRed: color | #d31014; + --defaultButtonBevel: shadow | $borderSide(#FFFFFF, top, 0.2) | $borderSide(#000000, bottom, 0.2); + --defaultButtonHoverGlow: shadow | 0 0 4 --text; + --defaultButtonShadow: shadow | 0 0 2 #000000; + --defaultInputBevel: shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2); + --fg: color | #151e2b; + --font: generic | sans-serif; + --link: color | #e2b188; + --monoFont: generic | monospace; + --pressedButtonBevel: shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2); + --selectionBackground: color | --accent; + --selectionText: color | $textColor(--accent, --text, no-preserve); + --text: color | #b9b9ba; + --wallpaper: color | #0c1118; + background: transparent; + opacity: 0; + } + + Root Underlay { + background: #000000; + opacity: 0.6; + } + + Root Underlay, test { + background: #000000; + opacity: 0.6; + } + ``` + +describe.only('html_tree_converter', () => { + describe('convertHtmlToTree', () => { + it('should parse ISS correctly', () => { + console.log(deserialize(testData)) + }) + }) +}) -- cgit v1.2.3-70-g09d2 From 0c91c376456520b0b58779696067bbf78e46bfd0 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 19 Sep 2024 21:42:14 +0300 Subject: somehow i lost this file and had to rewrite it. now it's even better than before! --- src/services/theme_data/iss_serializer.js | 38 +++++++++++++++++++++++++ src/services/theme_data/theme_data_3.service.js | 11 ------- 2 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 src/services/theme_data/iss_serializer.js (limited to 'src') diff --git a/src/services/theme_data/iss_serializer.js b/src/services/theme_data/iss_serializer.js new file mode 100644 index 00000000..8d6e9333 --- /dev/null +++ b/src/services/theme_data/iss_serializer.js @@ -0,0 +1,38 @@ +import { unroll } from './iss_utils.js' + +const serializeShadow = s => `{${s.inset ? 'inset ' : ''}${s.x} ${s.y} ${s.blur} ${s.spread} ${s.color} / ${s.alpha}}` + +export const serialize = (ruleset) => { + return ruleset.map((rule) => { + if (Object.keys(rule.directives || {}).length === 0) return false + + const header = unroll(rule).reverse().map(rule => { + const { component } = rule + const newVariant = rule.variant === 'normal' ? '' : ('.' + rule.variant) + const newState = rule.state.filter(st => st !== 'normal') + + return `${component}${newVariant}${newState.map(st => ':' + st).join('')}` + }).join(' ') + + const content = Object.entries(rule.directives).map(([directive, value]) => { + if (directive.startsWith('--')) { + const [valType, newValue] = value.split('|') // only first one! intentional! + switch (valType) { + case 'shadow': + return ` ${directive}: ${newValue.map(serializeShadow).join(', ')}` + default: + return ` ${directive}: ${newValue}` + } + } else { + switch (directive) { + case 'shadow': + return ` ${directive}: ${value.map(serializeShadow).join(', ')}` + default: + return ` ${directive}: ${value}` + } + } + }) + + return `${header} {\n${content.join(';\n')}\n}` + }).filter(x => x).join('\n\n') +} diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index cac450a8..b98cbb98 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -519,17 +519,6 @@ export const init = ({ } }) - const serializedData = serialize(eager) - const file = new File(serializedData, 'ruleset.piss') - const blobUrl = URL.createObjectURL(file) - const a = document.createElement('a') - a.href = blobUrl - a.download = 'ruleset.piss' - document.body.appendChild(a) - a.dispatchEvent(new MouseEvent('click')) - URL.revokeObjectURL(blobUrl) - document.body.removeChild(a) - return { lazy, eager, -- cgit v1.2.3-70-g09d2 From 71a478108035ddf3489d75bec6ef11c91829ab27 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 20 Sep 2024 02:05:25 +0300 Subject: at last... it's complete --- src/components/button.style.js | 4 +- src/services/theme_data/iss_deserializer.js | 126 ++++++++++++++++++--- src/services/theme_data/iss_serializer.js | 16 ++- src/services/theme_data/theme_data_3.service.js | 3 - .../services/theme_data/iss_deserializer.spec.js | 51 ++------- 5 files changed, 136 insertions(+), 64 deletions(-) (limited to 'src') diff --git a/src/components/button.style.js b/src/components/button.style.js index 6fec67a0..1bee8f8e 100644 --- a/src/components/button.style.js +++ b/src/components/button.style.js @@ -34,8 +34,8 @@ export default { directives: { '--defaultButtonHoverGlow': 'shadow | 0 0 4 --text', '--defaultButtonShadow': 'shadow | 0 0 2 #000000', - '--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2) | $borderSide(#000000, bottom, 0.2)', - '--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2)' + '--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2), $borderSide(#000000, bottom, 0.2)', + '--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)' } }, { diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js index 431e1b94..3cd2f15f 100644 --- a/src/services/theme_data/iss_deserializer.js +++ b/src/services/theme_data/iss_deserializer.js @@ -1,24 +1,51 @@ +import { flattenDeep } from 'lodash' + +const parseShadow = string => { + const modes = ['_full', 'inset', 'x', 'y', 'blur', 'spread', 'color', 'alpha'] + const regexPrep = [ + // inset keyword (optional) + '^(?:(inset)\\s+)?', + // x + '(?:([0-9]+(?:\\.[0-9]+)?)\\s+)', + // y + '(?:([0-9]+(?:\\.[0-9]+)?)\\s+)', + // blur (optional) + '(?:([0-9]+(?:\\.[0-9]+)?)\\s+)?', + // spread (optional) + '(?:([0-9]+(?:\\.[0-9]+)?)\\s+)?', + // either hex, variable or function + '(#[0-9a-f]{6}|--[a-z\\-_]+|\\$[a-z\\-()_]+)', + // opacity (optional) + '(?:\\s+\\/\\s+([0-9]+(?:\\.[0-9]+)?)\\s*)?$' + ].join('') + const regex = new RegExp(regexPrep, 'gis') // global, (stable) indices, single-string + const result = regex.exec(string) + if (result == null) { + return string + } else { + return Object.fromEntries(modes.map((mode, i) => [mode, result[i]])) + } +} // this works nearly the same as HTML tree converter -export const deserialize = (input) => { - const buffer = [] +const parseIss = (input) => { + const buffer = [{ selector: null, content: [] }] let textBuffer = '' const getCurrentBuffer = () => { - let current = buffer[buffer.length - 1][1] + let current = buffer[buffer.length - 1] if (current == null) { - current = { name: null, content: [] } + current = { selector: null, content: [] } } - buffer.push(current) return current } // Processes current line buffer, adds it to output buffer and clears line buffer - const flushText = (content) => { + const flushText = (kind) => { if (textBuffer === '') return - if (content) { - getCurrentBuffer().content.push(textBuffer) + if (kind === 'content') { + getCurrentBuffer().content.push(textBuffer.trim()) } else { - getCurrentBuffer().name = textBuffer + getCurrentBuffer().selector = textBuffer.trim() } textBuffer = '' } @@ -27,17 +54,90 @@ export const deserialize = (input) => { const char = input[i] if (char === ';') { - flushText(true) + flushText('content') } else if (char === '{') { - flushText(false) + flushText('header') } else if (char === '}') { - buffer.push({ name: null, content: [] }) + flushText('content') + buffer.push({ selector: null, content: [] }) textBuffer = '' } else { textBuffer += char } } - flushText() return buffer } +export const deserialize = (input) => { + const ast = parseIss(input) + const finalResult = ast.filter(i => i.selector != null).map(item => { + const { selector, content } = item + let stateCount = 0 + const selectors = selector.split(/,/g) + const result = selectors.map(selector => { + const output = { component: '' } + let currentDepth = null + + selector.split(/ /g).reverse().forEach((fragment, index, arr) => { + const fragmentObject = { component: '' } + + let mode = 'component' + for (let i = 0; i < fragment.length; i++) { + const char = fragment[i] + switch (char) { + case '.': { + mode = 'variant' + fragmentObject.variant = '' + break + } + case ':': { + mode = 'state' + fragmentObject.state = fragmentObject.state || [] + stateCount++ + break + } + default: { + if (mode === 'state') { + const currentState = fragmentObject.state[stateCount - 1] + if (currentState == null) { + fragmentObject.state.push('') + } + fragmentObject.state[stateCount - 1] += char + } else { + fragmentObject[mode] += char + } + } + } + } + if (currentDepth !== null) { + currentDepth.parent = { ...fragmentObject } + currentDepth = currentDepth.parent + } else { + Object.keys(fragmentObject).forEach(key => { + output[key] = fragmentObject[key] + }) + if (index !== (arr.length - 1)) { + output.parent = { component: '' } + } + currentDepth = output + } + }) + + output.directives = Object.fromEntries(content.map(d => { + const [property, value] = d.split(':') + console.log(property, value) + let realValue = value.trim() + if (property === 'shadow') { + realValue = parseShadow(value.split(',').map(v => v.trim())) + } if (!Number.isNaN(Number(value))) { + realValue = Number(value) + } + return [property, realValue] + })) + + return output + }) + return result + }) + return flattenDeep(finalResult) +} diff --git a/src/services/theme_data/iss_serializer.js b/src/services/theme_data/iss_serializer.js index 8d6e9333..6bba85e4 100644 --- a/src/services/theme_data/iss_serializer.js +++ b/src/services/theme_data/iss_serializer.js @@ -1,6 +1,12 @@ import { unroll } from './iss_utils.js' -const serializeShadow = s => `{${s.inset ? 'inset ' : ''}${s.x} ${s.y} ${s.blur} ${s.spread} ${s.color} / ${s.alpha}}` +const serializeShadow = s => { + if (typeof s === 'object') { + return `{${s.inset ? 'inset ' : ''}${s.x} ${s.y} ${s.blur} ${s.spread} ${s.color} / ${s.alpha}}` + } else { + return s + } +} export const serialize = (ruleset) => { return ruleset.map((rule) => { @@ -8,8 +14,8 @@ export const serialize = (ruleset) => { const header = unroll(rule).reverse().map(rule => { const { component } = rule - const newVariant = rule.variant === 'normal' ? '' : ('.' + rule.variant) - const newState = rule.state.filter(st => st !== 'normal') + const newVariant = (rule.variant == null || rule.variant === 'normal') ? '' : ('.' + rule.variant) + const newState = (rule.state || []).filter(st => st !== 'normal') return `${component}${newVariant}${newState.map(st => ':' + st).join('')}` }).join(' ') @@ -19,9 +25,9 @@ export const serialize = (ruleset) => { const [valType, newValue] = value.split('|') // only first one! intentional! switch (valType) { case 'shadow': - return ` ${directive}: ${newValue.map(serializeShadow).join(', ')}` + return ` ${directive}: ${valType.trim()} | ${newValue.map(serializeShadow).map(s => s.trim()).join(', ')}` default: - return ` ${directive}: ${newValue}` + return ` ${directive}: ${valType.trim()} | ${newValue.trim()}` } } else { switch (directive) { diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index b98cbb98..39c8b74f 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -23,9 +23,6 @@ import { findRules } from './iss_utils.js' import { parseCssShadow } from './css_utils.js' -import { - serialize -} from './iss_serializer.js' // Ensuring the order of components const components = { diff --git a/test/unit/specs/services/theme_data/iss_deserializer.spec.js b/test/unit/specs/services/theme_data/iss_deserializer.spec.js index f1967144..3488801c 100644 --- a/test/unit/specs/services/theme_data/iss_deserializer.spec.js +++ b/test/unit/specs/services/theme_data/iss_deserializer.spec.js @@ -1,47 +1,16 @@ import { deserialize } from 'src/services/theme_data/iss_deserializer.js' +import { serialize } from 'src/services/theme_data/iss_serializer.js' +import Button from 'src/components/button.style.js' -/* eslint-disable quotes */ -const testData = ``` - Root { - --accent: color | #e2b188; - --badgeNotification: color | #e15932; - --bg: color | #0f161e; - --cBlue: color | #81beea; - --cGreen: color | #5dc94a; - --cOrange: color | #ffc459; - --cRed: color | #d31014; - --defaultButtonBevel: shadow | $borderSide(#FFFFFF, top, 0.2) | $borderSide(#000000, bottom, 0.2); - --defaultButtonHoverGlow: shadow | 0 0 4 --text; - --defaultButtonShadow: shadow | 0 0 2 #000000; - --defaultInputBevel: shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2); - --fg: color | #151e2b; - --font: generic | sans-serif; - --link: color | #e2b188; - --monoFont: generic | monospace; - --pressedButtonBevel: shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2); - --selectionBackground: color | --accent; - --selectionText: color | $textColor(--accent, --text, no-preserve); - --text: color | #b9b9ba; - --wallpaper: color | #0c1118; - background: transparent; - opacity: 0; - } +describe.only('ISS (de)serialization', () => { + describe('ISS deserialization', () => { + it('Output should = input', () => { + const normalized = Button.defaultRules.map(x => ({ component: 'Button', ...x })) + const serialized = serialize(normalized) + const deserialized = deserialize(serialized) + // deserialized.toString() - Root Underlay { - background: #000000; - opacity: 0.6; - } - - Root Underlay, test { - background: #000000; - opacity: 0.6; - } - ``` - -describe.only('html_tree_converter', () => { - describe('convertHtmlToTree', () => { - it('should parse ISS correctly', () => { - console.log(deserialize(testData)) + expect(JSON.stringify(deserialized)).to.equal(JSON.stringify(normalized)) }) }) }) -- cgit v1.2.3-70-g09d2 From d8d766932a8b3caff7634ca1f9679525ce7123f1 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 20 Sep 2024 02:07:27 +0300 Subject: cleanup --- src/services/theme_data/iss_deserializer.js | 1 - test/unit/specs/services/theme_data/iss_deserializer.spec.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js index 3cd2f15f..8d4c987e 100644 --- a/src/services/theme_data/iss_deserializer.js +++ b/src/services/theme_data/iss_deserializer.js @@ -125,7 +125,6 @@ export const deserialize = (input) => { output.directives = Object.fromEntries(content.map(d => { const [property, value] = d.split(':') - console.log(property, value) let realValue = value.trim() if (property === 'shadow') { realValue = parseShadow(value.split(',').map(v => v.trim())) diff --git a/test/unit/specs/services/theme_data/iss_deserializer.spec.js b/test/unit/specs/services/theme_data/iss_deserializer.spec.js index 3488801c..d7d96130 100644 --- a/test/unit/specs/services/theme_data/iss_deserializer.spec.js +++ b/test/unit/specs/services/theme_data/iss_deserializer.spec.js @@ -4,12 +4,12 @@ import Button from 'src/components/button.style.js' describe.only('ISS (de)serialization', () => { describe('ISS deserialization', () => { - it('Output should = input', () => { + it('Output should equal input', () => { const normalized = Button.defaultRules.map(x => ({ component: 'Button', ...x })) const serialized = serialize(normalized) const deserialized = deserialize(serialized) - // deserialized.toString() + // for some reason comparing objects directly fails the assert expect(JSON.stringify(deserialized)).to.equal(JSON.stringify(normalized)) }) }) -- cgit v1.2.3-70-g09d2 From 48f0a95a3bf53321d950f23e480607cf94349751 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 20 Sep 2024 12:50:05 +0300 Subject: more tests, fixed some issues --- src/components/alert.style.js | 4 ++- src/components/button_unstyled.style.js | 3 +- src/components/input.style.js | 2 +- src/components/panel_header.style.js | 1 - src/services/theme_data/iss_deserializer.js | 23 ++++++++++---- src/services/theme_data/iss_serializer.js | 2 +- .../services/theme_data/iss_deserializer.spec.js | 36 ++++++++++++++++++---- 7 files changed, 53 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/components/alert.style.js b/src/components/alert.style.js index 19bd4bbb..abbeb5ba 100644 --- a/src/components/alert.style.js +++ b/src/components/alert.style.js @@ -27,7 +27,9 @@ export default { component: 'Alert' }, component: 'Border', - textColor: '--parent' + directives: { + textColor: '--parent' + } }, { variant: 'error', diff --git a/src/components/button_unstyled.style.js b/src/components/button_unstyled.style.js index 65b5c57b..435f9cc6 100644 --- a/src/components/button_unstyled.style.js +++ b/src/components/button_unstyled.style.js @@ -16,8 +16,7 @@ export default { { directives: { background: '#ffffff', - opacity: 0, - shadow: [] + opacity: 0 } }, { diff --git a/src/components/input.style.js b/src/components/input.style.js index 139a0034..7302cd6d 100644 --- a/src/components/input.style.js +++ b/src/components/input.style.js @@ -26,7 +26,7 @@ export default { { component: 'Root', directives: { - '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2)' + '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)' } }, { diff --git a/src/components/panel_header.style.js b/src/components/panel_header.style.js index 32464bc5..7c145758 100644 --- a/src/components/panel_header.style.js +++ b/src/components/panel_header.style.js @@ -17,7 +17,6 @@ export default { directives: { backgroundNoCssColor: 'yes', background: '--fg', - shadow: [] } } ] diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js index 8d4c987e..5d71f35f 100644 --- a/src/services/theme_data/iss_deserializer.js +++ b/src/services/theme_data/iss_deserializer.js @@ -6,13 +6,13 @@ const parseShadow = string => { // inset keyword (optional) '^(?:(inset)\\s+)?', // x - '(?:([0-9]+(?:\\.[0-9]+)?)\\s+)', + '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)', // y - '(?:([0-9]+(?:\\.[0-9]+)?)\\s+)', + '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)', // blur (optional) - '(?:([0-9]+(?:\\.[0-9]+)?)\\s+)?', + '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)?', // spread (optional) - '(?:([0-9]+(?:\\.[0-9]+)?)\\s+)?', + '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)?', // either hex, variable or function '(#[0-9a-f]{6}|--[a-z\\-_]+|\\$[a-z\\-()_]+)', // opacity (optional) @@ -23,7 +23,18 @@ const parseShadow = string => { if (result == null) { return string } else { - return Object.fromEntries(modes.map((mode, i) => [mode, result[i]])) + const numeric = new Set(['x', 'y', 'blur', 'spread', 'alpha']) + const { x, y, blur, spread, alpha, inset, color } = Object.fromEntries(modes.map((mode, i) => { + if (numeric.has(mode)) { + return [mode, Number(result[i])] + } else if (mode === 'inset') { + return [mode, !!result[i]] + } else { + return [mode, result[i]] + } + }).filter(([k, v]) => v !== false).slice(1)) + + return { x, y, blur, spread, color, alpha, inset } } } // this works nearly the same as HTML tree converter @@ -127,7 +138,7 @@ export const deserialize = (input) => { const [property, value] = d.split(':') let realValue = value.trim() if (property === 'shadow') { - realValue = parseShadow(value.split(',').map(v => v.trim())) + realValue = value.split(',').map(v => parseShadow(v.trim())) } if (!Number.isNaN(Number(value))) { realValue = Number(value) } diff --git a/src/services/theme_data/iss_serializer.js b/src/services/theme_data/iss_serializer.js index 6bba85e4..959852b7 100644 --- a/src/services/theme_data/iss_serializer.js +++ b/src/services/theme_data/iss_serializer.js @@ -2,7 +2,7 @@ import { unroll } from './iss_utils.js' const serializeShadow = s => { if (typeof s === 'object') { - return `{${s.inset ? 'inset ' : ''}${s.x} ${s.y} ${s.blur} ${s.spread} ${s.color} / ${s.alpha}}` + return `${s.inset ? 'inset ' : ''}${s.x} ${s.y} ${s.blur} ${s.spread} ${s.color} / ${s.alpha}` } else { return s } diff --git a/test/unit/specs/services/theme_data/iss_deserializer.spec.js b/test/unit/specs/services/theme_data/iss_deserializer.spec.js index 7654fb53..6141015c 100644 --- a/test/unit/specs/services/theme_data/iss_deserializer.spec.js +++ b/test/unit/specs/services/theme_data/iss_deserializer.spec.js @@ -1,16 +1,40 @@ import { deserialize } from 'src/services/theme_data/iss_deserializer.js' import { serialize } from 'src/services/theme_data/iss_serializer.js' -import Button from 'src/components/button.style.js' +const componentsContext = require.context('src', true, /\.style.js(on)?$/) -describe('ISS (de)serialization', () => { - describe('ISS deserialization', () => { - it('Output should equal input', () => { - const normalized = Button.defaultRules.map(x => ({ component: 'Button', ...x })) +describe.only('ISS (de)serialization', () => { + componentsContext.keys().forEach(key => { + const component = componentsContext(key).default + + it(`(De)serialization of component ${component.name} works`, () => { + const normalized = component.defaultRules.map(x => ({ component: component.name, ...x })) const serialized = serialize(normalized) const deserialized = deserialize(serialized) // for some reason comparing objects directly fails the assert - expect(JSON.stringify(deserialized)).to.equal(JSON.stringify(normalized)) + expect(JSON.stringify(deserialized, null, 2)).to.equal(JSON.stringify(normalized, null, 2)) }) }) + + /* + // Debug snippet + const onlyComponent = componentsContext('./components/panel_header.style.js').default + it(`(De)serialization of component ${onlyComponent.name} works`, () => { + const normalized = onlyComponent.defaultRules.map(x => ({ component: onlyComponent.name, ...x })) + console.log('BEGIN INPUT ================') + console.log(normalized) + console.log('END INPUT ==================') + const serialized = serialize(normalized) + console.log('BEGIN SERIAL ===============') + console.log(serialized) + console.log('END SERIAL =================') + const deserialized = deserialize(serialized) + console.log('BEGIN DESERIALIZED =========') + console.log(serialized) + console.log('END DESERIALIZED ===========') + + // for some reason comparing objects directly fails the assert + expect(JSON.stringify(deserialized, null, 2)).to.equal(JSON.stringify(normalized, null, 2)) + }) + */ }) -- cgit v1.2.3-70-g09d2 From b5da1f8b89ddbfcb7bb5ad0c53005ddd93c1ba48 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 20 Sep 2024 12:54:47 +0300 Subject: fix lint --- src/components/panel_header.style.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/components/panel_header.style.js b/src/components/panel_header.style.js index 7c145758..010e42cd 100644 --- a/src/components/panel_header.style.js +++ b/src/components/panel_header.style.js @@ -16,7 +16,7 @@ export default { component: 'PanelHeader', directives: { backgroundNoCssColor: 'yes', - background: '--fg', + background: '--fg' } } ] -- cgit v1.2.3-70-g09d2 From ab8907909b1cb02b091e6ed22b7a2806bc837f33 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 24 Sep 2024 00:23:25 +0300 Subject: add 'none' keyword to PISS shadow definiton that equals empty array --- src/components/button_unstyled.style.js | 3 ++- src/components/panel_header.style.js | 3 ++- src/services/theme_data/iss_deserializer.js | 6 +++++- src/services/theme_data/iss_serializer.js | 6 +++++- test/unit/specs/services/theme_data/iss_deserializer.spec.js | 4 ++-- 5 files changed, 16 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/components/button_unstyled.style.js b/src/components/button_unstyled.style.js index 435f9cc6..65b5c57b 100644 --- a/src/components/button_unstyled.style.js +++ b/src/components/button_unstyled.style.js @@ -16,7 +16,8 @@ export default { { directives: { background: '#ffffff', - opacity: 0 + opacity: 0, + shadow: [] } }, { diff --git a/src/components/panel_header.style.js b/src/components/panel_header.style.js index 010e42cd..32464bc5 100644 --- a/src/components/panel_header.style.js +++ b/src/components/panel_header.style.js @@ -16,7 +16,8 @@ export default { component: 'PanelHeader', directives: { backgroundNoCssColor: 'yes', - background: '--fg' + background: '--fg', + shadow: [] } } ] diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js index 5d71f35f..909e9411 100644 --- a/src/services/theme_data/iss_deserializer.js +++ b/src/services/theme_data/iss_deserializer.js @@ -138,7 +138,11 @@ export const deserialize = (input) => { const [property, value] = d.split(':') let realValue = value.trim() if (property === 'shadow') { - realValue = value.split(',').map(v => parseShadow(v.trim())) + if (realValue === 'none') { + realValue = [] + } else { + realValue = value.split(',').map(v => parseShadow(v.trim())) + } } if (!Number.isNaN(Number(value))) { realValue = Number(value) } diff --git a/src/services/theme_data/iss_serializer.js b/src/services/theme_data/iss_serializer.js index 959852b7..8b7cf5d8 100644 --- a/src/services/theme_data/iss_serializer.js +++ b/src/services/theme_data/iss_serializer.js @@ -32,7 +32,11 @@ export const serialize = (ruleset) => { } else { switch (directive) { case 'shadow': - return ` ${directive}: ${value.map(serializeShadow).join(', ')}` + if (value.length > 0) { + return ` ${directive}: ${value.map(serializeShadow).join(', ')}` + } else { + return ` ${directive}: none` + } default: return ` ${directive}: ${value}` } diff --git a/test/unit/specs/services/theme_data/iss_deserializer.spec.js b/test/unit/specs/services/theme_data/iss_deserializer.spec.js index 01f8dacf..6eb25dfe 100644 --- a/test/unit/specs/services/theme_data/iss_deserializer.spec.js +++ b/test/unit/specs/services/theme_data/iss_deserializer.spec.js @@ -19,7 +19,7 @@ describe('ISS (de)serialization', () => { /* // Debug snippet const onlyComponent = componentsContext('./components/panel_header.style.js').default - it(`(De)serialization of component ${onlyComponent.name} works`, () => { + it.only(`(De)serialization of component ${onlyComponent.name} works`, () => { const normalized = onlyComponent.defaultRules.map(x => ({ component: onlyComponent.name, ...x })) console.log('BEGIN INPUT ================') console.log(normalized) @@ -36,5 +36,5 @@ describe('ISS (de)serialization', () => { // for some reason comparing objects directly fails the assert expect(JSON.stringify(deserialized, null, 2)).to.equal(JSON.stringify(normalized, null, 2)) }) - */ + /* */ }) -- cgit v1.2.3-70-g09d2 From 6335a205c80336c70f4ac4d68e6f3ec305994070 Mon Sep 17 00:00:00 2001 From: Kian-ting Tan Date: Wed, 11 Sep 2024 12:34:58 +0000 Subject: Translated using Weblate (Chinese (Min Nan) (nan_TW)) Currently translated at 91.6% (1093 of 1193 strings) Translation: Pleroma/Pleroma-FE Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/nan_TW/ --- src/i18n/nan-TW.json | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/i18n/nan-TW.json b/src/i18n/nan-TW.json index 782a75f5..44a693dd 100644 --- a/src/i18n/nan-TW.json +++ b/src/i18n/nan-TW.json @@ -190,7 +190,8 @@ "mobile_notifications_close": "關掉通知", "announcements": "公告", "search": "Tshuē", - "mobile_notifications_mark_as_seen": "Lóng 標做有讀" + "mobile_notifications_mark_as_seen": "Lóng 標做有讀", + "quotes": "引用" }, "notifications": { "broken_favorite": "狀態毋知影,leh tshiau-tshuē……", @@ -212,7 +213,8 @@ "unread_follow_requests": "{num}ê新ê跟tuè請求", "configuration_tip": "用{theSettings},lí通自訂siánn物佇tsia顯示。{dismiss}", "configuration_tip_settings": "設定", - "configuration_tip_dismiss": "Mài koh顯示" + "configuration_tip_dismiss": "Mài koh顯示", + "subscribed_status": "有發送ê" }, "polls": { "add_poll": "開投票", @@ -252,7 +254,8 @@ }, "load_all_hint": "載入頭前 {saneAmount} ê 繪文字,規个攏載入效能可能 ē khah 食力。", "load_all": "Kā {emojiAmount} ê 繪文字攏載入", - "regional_indicator": "地區指引 {letter}" + "regional_indicator": "地區指引 {letter}", + "hide_custom_emoji": "Khàm掉自訂ê繪文字" }, "errors": { "storage_unavailable": "Pleroma buē-tàng the̍h 著瀏覽器儲存 ê。Lí ê 登入狀態抑是局部設定 buē 儲存,mā 凡勢 tú 著意料外 ê 問題。拍開 cookie 看māi。" @@ -263,7 +266,8 @@ "emoji_reactions": "繪文字 ê 反應", "reports": "檢舉", "moves": "用者 ê 移民", - "load_older": "載入 koh khah 早 ê 互動" + "load_older": "載入 koh khah 早 ê 互動", + "statuses": "訂ê" }, "post_status": { "edit_status": "編輯狀態", @@ -935,7 +939,34 @@ "notification_extra_chats": "顯示bô讀ê開講", "notification_extra_announcements": "顯示bô讀ê公告", "notification_extra_follow_requests": "顯示新ê跟tuè請求", - "notification_extra_tip": "顯示自訂其他通知ê撇步" + "notification_extra_tip": "顯示自訂其他通知ê撇步", + "confirm_new_setting": "Lí敢確認新ê設定?", + "text_size_tip": "用 {0} 做絕對值,{1} ē根據瀏覽器ê標準文字sài-suh放大縮小。", + "theme_debug": "佇處理透明ê時,顯示背景主題ia̋n-jín 所假使ê(DEBUG)", + "units": { + "time": { + "s": "秒鐘", + "m": "分鐘", + "h": "點鐘", + "d": "工" + } + }, + "actor_type": "Tsit ê口座是:", + "actor_type_Person": "一般ê用者", + "actor_type_description": "標記lí ê口座做群組,ē hōo自動轉送提起伊ê狀態。", + "actor_type_Group": "群組", + "actor_type_Service": "機器lâng", + "appearance": "外觀", + "confirm_new_question": "Tse看起來kám好?設定ē佇10秒鐘後改轉去。", + "revert": "改轉去", + "confirm": "確認", + "text_size": "文字kap界面ê sài-suh", + "text_size_tip2": "毋是 {0} ê值可能ē破壞一寡物件kap主題", + "emoji_size": "繪文字ê sài-suh", + "navbar_size": "頂 liâu-á êsài-suh", + "panel_header_size": "面pang標題ê sài-suh", + "visual_tweaks": "細細ê外觀調整", + "scale_and_layout": "界面ê sài-suh kap排列" }, "status": { "favorites": "收藏", @@ -1001,7 +1032,7 @@ "show_only_conversation_under_this": "Kan-ta顯示tsit ê狀態ê回應", "status_history": "狀態ê歷史", "reaction_count_label": "{num}ê lâng用表情反應", - "hide_quote": "Khàm條引用ê狀態", + "hide_quote": "Khàm掉引用ê狀態", "display_quote": "顯示引用ê狀態", "invisible_quote": "引用ê狀態bē當用:{link}", "more_actions": "佇tsit ê狀態ê其他動作" -- cgit v1.2.3-70-g09d2