diff options
Diffstat (limited to 'test/options-util.test.js')
| -rw-r--r-- | test/options-util.test.js | 1587 | 
1 files changed, 1587 insertions, 0 deletions
diff --git a/test/options-util.test.js b/test/options-util.test.js new file mode 100644 index 00000000..9f49eb28 --- /dev/null +++ b/test/options-util.test.js @@ -0,0 +1,1587 @@ +/* + * Copyright (C) 2023  Yomitan Authors + * Copyright (C) 2020-2022  Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <https://www.gnu.org/licenses/>. + */ + +import fs from 'fs'; +import url, {fileURLToPath} from 'node:url'; +import path from 'path'; +import {expect, test, vi} from 'vitest'; +import {OptionsUtil} from '../ext/js/data/options-util.js'; +import {TemplatePatcher} from '../ext/js/templates/template-patcher.js'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); +vi.stubGlobal('fetch', async function fetch(url2) { +    const filePath = url.fileURLToPath(url2); +    await Promise.resolve(); +    const content = fs.readFileSync(filePath, {encoding: null}); +    return { +        ok: true, +        status: 200, +        statusText: 'OK', +        text: async () => Promise.resolve(content.toString('utf8')), +        json: async () => Promise.resolve(JSON.parse(content.toString('utf8'))) +    }; +}); +vi.stubGlobal('chrome', { +    runtime: { +        getURL: (path2) => { +            return url.pathToFileURL(path.join(dirname, '..', 'ext', path2.replace(/^\//, ''))).href; +        } +    } +}); + +function createProfileOptionsTestData1() { +    return { +        version: 14, +        general: { +            enable: true, +            enableClipboardPopups: false, +            resultOutputMode: 'group', +            debugInfo: false, +            maxResults: 32, +            showAdvanced: false, +            popupDisplayMode: 'default', +            popupWidth: 400, +            popupHeight: 250, +            popupHorizontalOffset: 0, +            popupVerticalOffset: 10, +            popupHorizontalOffset2: 10, +            popupVerticalOffset2: 0, +            popupHorizontalTextPosition: 'below', +            popupVerticalTextPosition: 'before', +            popupScalingFactor: 1, +            popupScaleRelativeToPageZoom: false, +            popupScaleRelativeToVisualViewport: true, +            showGuide: true, +            compactTags: false, +            compactGlossaries: false, +            mainDictionary: '', +            popupTheme: 'default', +            popupOuterTheme: 'default', +            customPopupCss: '', +            customPopupOuterCss: '', +            enableWanakana: true, +            enableClipboardMonitor: false, +            showPitchAccentDownstepNotation: true, +            showPitchAccentPositionNotation: true, +            showPitchAccentGraph: false, +            showIframePopupsInRootFrame: false, +            useSecurePopupFrameUrl: true, +            usePopupShadowDom: true +        }, +        audio: { +            enabled: true, +            sources: ['jpod101', 'text-to-speech', 'custom'], +            volume: 100, +            autoPlay: false, +            customSourceUrl: 'http://localhost/audio.mp3?term={expression}&reading={reading}', +            textToSpeechVoice: 'example-voice' +        }, +        scanning: { +            middleMouse: true, +            touchInputEnabled: true, +            selectText: true, +            alphanumeric: true, +            autoHideResults: false, +            delay: 20, +            length: 10, +            modifier: 'shift', +            deepDomScan: false, +            popupNestingMaxDepth: 0, +            enablePopupSearch: false, +            enableOnPopupExpressions: false, +            enableOnSearchPage: true, +            enableSearchTags: false, +            layoutAwareScan: false +        }, +        translation: { +            convertHalfWidthCharacters: 'false', +            convertNumericCharacters: 'false', +            convertAlphabeticCharacters: 'false', +            convertHiraganaToKatakana: 'false', +            convertKatakanaToHiragana: 'variant', +            collapseEmphaticSequences: 'false' +        }, +        dictionaries: { +            'Test Dictionary': { +                priority: 0, +                enabled: true, +                allowSecondarySearches: false +            } +        }, +        parsing: { +            enableScanningParser: true, +            enableMecabParser: false, +            selectedParser: null, +            termSpacing: true, +            readingMode: 'hiragana' +        }, +        anki: { +            enable: false, +            server: 'http://127.0.0.1:8765', +            tags: ['yomichan'], +            sentenceExt: 200, +            screenshot: {format: 'png', quality: 92}, +            terms: {deck: '', model: '', fields: {}}, +            kanji: {deck: '', model: '', fields: {}}, +            duplicateScope: 'collection', +            fieldTemplates: null +        } +    }; +} + +function createOptionsTestData1() { +    return { +        profiles: [ +            { +                name: 'Default', +                options: createProfileOptionsTestData1(), +                conditionGroups: [ +                    { +                        conditions: [ +                            { +                                type: 'popupLevel', +                                operator: 'equal', +                                value: 1 +                            }, +                            { +                                type: 'popupLevel', +                                operator: 'notEqual', +                                value: 0 +                            }, +                            { +                                type: 'popupLevel', +                                operator: 'lessThan', +                                value: 3 +                            }, +                            { +                                type: 'popupLevel', +                                operator: 'greaterThan', +                                value: 0 +                            }, +                            { +                                type: 'popupLevel', +                                operator: 'lessThanOrEqual', +                                value: 2 +                            }, +                            { +                                type: 'popupLevel', +                                operator: 'greaterThanOrEqual', +                                value: 1 +                            } +                        ] +                    }, +                    { +                        conditions: [ +                            { +                                type: 'url', +                                operator: 'matchDomain', +                                value: 'example.com' +                            }, +                            { +                                type: 'url', +                                operator: 'matchRegExp', +                                value: 'example\\.com' +                            } +                        ] +                    }, +                    { +                        conditions: [ +                            { +                                type: 'modifierKeys', +                                operator: 'are', +                                value: [ +                                    'ctrl', +                                    'shift' +                                ] +                            }, +                            { +                                type: 'modifierKeys', +                                operator: 'areNot', +                                value: [ +                                    'alt', +                                    'shift' +                                ] +                            }, +                            { +                                type: 'modifierKeys', +                                operator: 'include', +                                value: 'alt' +                            }, +                            { +                                type: 'modifierKeys', +                                operator: 'notInclude', +                                value: 'ctrl' +                            } +                        ] +                    } +                ] +            } +        ], +        profileCurrent: 0, +        version: 2, +        global: { +            database: { +                prefixWildcardsSupported: false +            } +        } +    }; +} + + +function createProfileOptionsUpdatedTestData1() { +    return { +        general: { +            enable: true, +            resultOutputMode: 'group', +            debugInfo: false, +            maxResults: 32, +            showAdvanced: false, +            popupDisplayMode: 'default', +            popupWidth: 400, +            popupHeight: 250, +            popupHorizontalOffset: 0, +            popupVerticalOffset: 10, +            popupHorizontalOffset2: 10, +            popupVerticalOffset2: 0, +            popupHorizontalTextPosition: 'below', +            popupVerticalTextPosition: 'before', +            popupScalingFactor: 1, +            popupScaleRelativeToPageZoom: false, +            popupScaleRelativeToVisualViewport: true, +            showGuide: true, +            compactTags: false, +            glossaryLayoutMode: 'default', +            mainDictionary: '', +            popupTheme: 'light', +            popupOuterTheme: 'light', +            customPopupCss: '', +            customPopupOuterCss: '', +            enableWanakana: true, +            showPitchAccentDownstepNotation: true, +            showPitchAccentPositionNotation: true, +            showPitchAccentGraph: false, +            showIframePopupsInRootFrame: false, +            useSecurePopupFrameUrl: true, +            usePopupShadowDom: true, +            usePopupWindow: false, +            popupCurrentIndicatorMode: 'triangle', +            popupActionBarVisibility: 'auto', +            popupActionBarLocation: 'top', +            frequencyDisplayMode: 'split-tags-grouped', +            termDisplayMode: 'ruby', +            sortFrequencyDictionary: null, +            sortFrequencyDictionaryOrder: 'descending' +        }, +        audio: { +            enabled: true, +            sources: [ +                { +                    type: 'jpod101', +                    url: '', +                    voice: '' +                }, +                { +                    type: 'text-to-speech', +                    url: '', +                    voice: 'example-voice' +                }, +                { +                    type: 'custom', +                    url: 'http://localhost/audio.mp3?term={term}&reading={reading}', +                    voice: '' +                } +            ], +            volume: 100, +            autoPlay: false +        }, +        scanning: { +            touchInputEnabled: true, +            selectText: true, +            alphanumeric: true, +            autoHideResults: false, +            delay: 20, +            length: 10, +            deepDomScan: false, +            popupNestingMaxDepth: 0, +            enablePopupSearch: false, +            enableOnPopupExpressions: false, +            enableOnSearchPage: true, +            enableSearchTags: false, +            layoutAwareScan: false, +            hideDelay: 0, +            pointerEventsEnabled: false, +            matchTypePrefix: false, +            hidePopupOnCursorExit: false, +            hidePopupOnCursorExitDelay: 0, +            normalizeCssZoom: true, +            preventMiddleMouse: { +                onWebPages: false, +                onPopupPages: false, +                onSearchPages: false, +                onSearchQuery: false +            }, +            inputs: [ +                { +                    include: 'shift', +                    exclude: 'mouse0', +                    types: { +                        mouse: true, +                        touch: false, +                        pen: false +                    }, +                    options: { +                        showAdvanced: false, +                        searchTerms: true, +                        searchKanji: true, +                        scanOnTouchMove: true, +                        scanOnTouchPress: true, +                        scanOnTouchRelease: false, +                        scanOnPenMove: true, +                        scanOnPenHover: true, +                        scanOnPenReleaseHover: false, +                        scanOnPenPress: true, +                        scanOnPenRelease: false, +                        preventTouchScrolling: true, +                        preventPenScrolling: true +                    } +                }, +                { +                    include: 'mouse2', +                    exclude: '', +                    types: { +                        mouse: true, +                        touch: false, +                        pen: false +                    }, +                    options: { +                        showAdvanced: false, +                        searchTerms: true, +                        searchKanji: true, +                        scanOnTouchMove: true, +                        scanOnTouchPress: true, +                        scanOnTouchRelease: false, +                        scanOnPenMove: true, +                        scanOnPenHover: true, +                        scanOnPenReleaseHover: false, +                        scanOnPenPress: true, +                        scanOnPenRelease: false, +                        preventTouchScrolling: true, +                        preventPenScrolling: true +                    } +                }, +                { +                    include: '', +                    exclude: '', +                    types: { +                        mouse: false, +                        touch: true, +                        pen: true +                    }, +                    options: { +                        showAdvanced: false, +                        searchTerms: true, +                        searchKanji: true, +                        scanOnTouchMove: true, +                        scanOnTouchPress: true, +                        scanOnTouchRelease: false, +                        scanOnPenMove: true, +                        scanOnPenHover: true, +                        scanOnPenReleaseHover: false, +                        scanOnPenPress: true, +                        scanOnPenRelease: false, +                        preventTouchScrolling: true, +                        preventPenScrolling: true +                    } +                } +            ] +        }, +        translation: { +            convertHalfWidthCharacters: 'false', +            convertNumericCharacters: 'false', +            convertAlphabeticCharacters: 'false', +            convertHiraganaToKatakana: 'false', +            convertKatakanaToHiragana: 'variant', +            collapseEmphaticSequences: 'false', +            textReplacements: { +                searchOriginal: true, +                groups: [] +            } +        }, +        dictionaries: [ +            { +                name: 'Test Dictionary', +                priority: 0, +                enabled: true, +                allowSecondarySearches: false, +                definitionsCollapsible: 'not-collapsible' +            } +        ], +        parsing: { +            enableScanningParser: true, +            enableMecabParser: false, +            selectedParser: null, +            termSpacing: true, +            readingMode: 'hiragana' +        }, +        anki: { +            enable: false, +            server: 'http://127.0.0.1:8765', +            tags: ['yomichan'], +            screenshot: {format: 'png', quality: 92}, +            terms: {deck: '', model: '', fields: {}}, +            kanji: {deck: '', model: '', fields: {}}, +            duplicateScope: 'collection', +            duplicateScopeCheckAllModels: false, +            displayTags: 'never', +            checkForDuplicates: true, +            fieldTemplates: null, +            suspendNewCards: false, +            noteGuiMode: 'browse', +            apiKey: '', +            downloadTimeout: 0 +        }, +        sentenceParsing: { +            scanExtent: 200, +            terminationCharacterMode: 'custom', +            terminationCharacters: [ +                {enabled: true, character1: '「', character2: '」', includeCharacterAtStart: false, includeCharacterAtEnd: false}, +                {enabled: true, character1: '『', character2: '』', includeCharacterAtStart: false, includeCharacterAtEnd: false}, +                {enabled: true, character1: '"', character2: '"', includeCharacterAtStart: false, includeCharacterAtEnd: false}, +                {enabled: true, character1: '\'', character2: '\'', includeCharacterAtStart: false, includeCharacterAtEnd: false}, +                {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, +                {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, +                {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, +                {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, +                {enabled: true, character1: '。', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, +                {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, +                {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, +                {enabled: true, character1: '…', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, +                {enabled: true, character1: '︒', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, +                {enabled: true, character1: '︕', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, +                {enabled: true, character1: '︖', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, +                {enabled: true, character1: '︙', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true} +            ] +        }, +        inputs: { +            hotkeys: [ +                {action: 'close',             argument: '',  key: 'Escape',    modifiers: [],       scopes: ['popup'], enabled: true}, +                {action: 'focusSearchBox',    argument: '',  key: 'Escape',    modifiers: [],       scopes: ['search'], enabled: true}, +                {action: 'previousEntry',     argument: '3', key: 'PageUp',    modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'nextEntry',         argument: '3', key: 'PageDown',  modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'lastEntry',         argument: '',  key: 'End',       modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'firstEntry',        argument: '',  key: 'Home',      modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'previousEntry',     argument: '1', key: 'ArrowUp',   modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'nextEntry',         argument: '1', key: 'ArrowDown', modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'historyBackward',   argument: '',  key: 'KeyB',      modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'historyForward',    argument: '',  key: 'KeyF',      modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'addNoteKanji',      argument: '',  key: 'KeyK',      modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'addNoteTermKanji',  argument: '',  key: 'KeyE',      modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'addNoteTermKana',   argument: '',  key: 'KeyR',      modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'playAudio',         argument: '',  key: 'KeyP',      modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'viewNote',          argument: '',  key: 'KeyV',      modifiers: ['alt'],  scopes: ['popup', 'search'], enabled: true}, +                {action: 'copyHostSelection', argument: '',  key: 'KeyC',      modifiers: ['ctrl'], scopes: ['popup'], enabled: true} +            ] +        }, +        popupWindow: { +            width: 400, +            height: 250, +            left: 0, +            top: 0, +            useLeft: false, +            useTop: false, +            windowType: 'popup', +            windowState: 'normal' +        }, +        clipboard: { +            enableBackgroundMonitor: false, +            enableSearchPageMonitor: false, +            autoSearchContent: true, +            maximumSearchLength: 1000 +        }, +        accessibility: { +            forceGoogleDocsHtmlRendering: false +        } +    }; +} + +function createOptionsUpdatedTestData1() { +    return { +        profiles: [ +            { +                name: 'Default', +                options: createProfileOptionsUpdatedTestData1(), +                conditionGroups: [ +                    { +                        conditions: [ +                            { +                                type: 'popupLevel', +                                operator: 'equal', +                                value: '1' +                            }, +                            { +                                type: 'popupLevel', +                                operator: 'notEqual', +                                value: '0' +                            }, +                            { +                                type: 'popupLevel', +                                operator: 'lessThan', +                                value: '3' +                            }, +                            { +                                type: 'popupLevel', +                                operator: 'greaterThan', +                                value: '0' +                            }, +                            { +                                type: 'popupLevel', +                                operator: 'lessThanOrEqual', +                                value: '2' +                            }, +                            { +                                type: 'popupLevel', +                                operator: 'greaterThanOrEqual', +                                value: '1' +                            } +                        ] +                    }, +                    { +                        conditions: [ +                            { +                                type: 'url', +                                operator: 'matchDomain', +                                value: 'example.com' +                            }, +                            { +                                type: 'url', +                                operator: 'matchRegExp', +                                value: 'example\\.com' +                            } +                        ] +                    }, +                    { +                        conditions: [ +                            { +                                type: 'modifierKeys', +                                operator: 'are', +                                value: 'ctrl, shift' +                            }, +                            { +                                type: 'modifierKeys', +                                operator: 'areNot', +                                value: 'alt, shift' +                            }, +                            { +                                type: 'modifierKeys', +                                operator: 'include', +                                value: 'alt' +                            }, +                            { +                                type: 'modifierKeys', +                                operator: 'notInclude', +                                value: 'ctrl' +                            } +                        ] +                    } +                ] +            } +        ], +        profileCurrent: 0, +        version: 21, +        global: { +            database: { +                prefixWildcardsSupported: false +            } +        } +    }; +} + + +async function testUpdate() { +    test('Update', async () => { +        const optionsUtil = new OptionsUtil(); +        await optionsUtil.prepare(); + +        const options = createOptionsTestData1(); +        const optionsUpdated = structuredClone(await optionsUtil.update(options)); +        const optionsExpected = createOptionsUpdatedTestData1(); +        expect(optionsUpdated).toStrictEqual(optionsExpected); +    }); +} + +async function testDefault() { +    test('Default', async () => { +        const data = [ +            (options) => options, +            (options) => { +                delete options.profiles[0].options.audio.autoPlay; +            }, +            (options) => { +                options.profiles[0].options.audio.autoPlay = void 0; +            } +        ]; + +        const optionsUtil = new OptionsUtil(); +        await optionsUtil.prepare(); + +        for (const modify of data) { +            const options = optionsUtil.getDefault(); + +            const optionsModified = structuredClone(options); +            modify(optionsModified); + +            const optionsUpdated = await optionsUtil.update(structuredClone(optionsModified)); +            expect(structuredClone(optionsUpdated)).toStrictEqual(structuredClone(options)); +        } +    }); +} + +async function testFieldTemplatesUpdate() { +    test('FieldTemplatesUpdate', async () => { +        const optionsUtil = new OptionsUtil(); +        await optionsUtil.prepare(); + +        const templatePatcher = new TemplatePatcher(); +        const loadDataFile = (fileName) => { +            const content = fs.readFileSync(path.join(dirname, '..', 'ext', fileName), {encoding: 'utf8'}); +            return templatePatcher.parsePatch(content).addition; +        }; +        const updates = [ +            {version: 2,  changes: loadDataFile('data/templates/anki-field-templates-upgrade-v2.handlebars')}, +            {version: 4,  changes: loadDataFile('data/templates/anki-field-templates-upgrade-v4.handlebars')}, +            {version: 6,  changes: loadDataFile('data/templates/anki-field-templates-upgrade-v6.handlebars')}, +            {version: 8,  changes: loadDataFile('data/templates/anki-field-templates-upgrade-v8.handlebars')}, +            {version: 10, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v10.handlebars')}, +            {version: 12, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v12.handlebars')}, +            {version: 13, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v13.handlebars')}, +            {version: 21, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v21.handlebars')} +        ]; +        const getUpdateAdditions = (startVersion, targetVersion) => { +            let value = ''; +            for (const {version, changes} of updates) { +                if (version <= startVersion || version > targetVersion || changes.length === 0) { continue; } +                if (value.length > 0) { value += '\n'; } +                value += changes; +            } +            return value; +        }; + +        const data = [ +        // Standard format +            { +                oldVersion: 0, +                newVersion: 12, +                old: ` +{{#*inline "character"}} +    {{~definition.character~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + +                expected: ` +{{#*inline "character"}} +    {{~definition.character~}} +{{/inline}} + +<<<UPDATE-ADDITIONS>>> +{{~> (lookup . "marker") ~}}`.trimStart() +            }, +            // Non-standard marker format +            { +                oldVersion: 0, +                newVersion: 12, +                old: ` +{{#*inline "character"}} +    {{~definition.character~}} +{{/inline}} + +{{~> (lookup . "marker2") ~}}`.trimStart(), + +                expected: ` +{{#*inline "character"}} +    {{~definition.character~}} +{{/inline}} + +{{~> (lookup . "marker2") ~}} +<<<UPDATE-ADDITIONS>>>`.trimStart() +            }, +            // Empty test +            { +                oldVersion: 0, +                newVersion: 12, +                old: ` +{{~> (lookup . "marker") ~}}`.trimStart(), + +                expected: ` +<<<UPDATE-ADDITIONS>>> +{{~> (lookup . "marker") ~}}`.trimStart() +            }, +            // Definition tags update +            { +                oldVersion: 0, +                newVersion: 12, +                old: ` +{{#*inline "glossary-single"}} +    {{~#unless brief~}} +        {{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}} +        {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} +    {{~/unless~}} +{{/inline}} + +{{#*inline "glossary-single2"}} +    {{~#unless brief~}} +        {{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}} +        {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} +    {{~/unless~}} +{{/inline}} + +{{#*inline "glossary"}} +    {{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries~}} +    {{~> glossary-single definition brief=brief compactGlossaries=../compactGlossaries~}} +{{/inline}} + +{{~> (lookup . "marker") ~}} +`.trimStart(), + +                expected: ` +{{#*inline "glossary-single"}} +    {{~#unless brief~}} +        {{~#scope~}} +            {{~#set "any" false}}{{/set~}} +            {{~#if definitionTags~}}{{#each definitionTags~}} +                {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}} +                    {{~#if (get "any")}}, {{else}}<i>({{/if~}} +                    {{name}} +                    {{~#set "any" true}}{{/set~}} +                {{~/if~}} +            {{~/each~}} +            {{~#if (get "any")}})</i> {{/if~}} +            {{~/if~}} +        {{~/scope~}} +        {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} +    {{~/unless~}} +{{/inline}} + +{{#*inline "glossary-single2"}} +    {{~#unless brief~}} +        {{~#scope~}} +            {{~#set "any" false}}{{/set~}} +            {{~#if definitionTags~}}{{#each definitionTags~}} +                {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}} +                    {{~#if (get "any")}}, {{else}}<i>({{/if~}} +                    {{name}} +                    {{~#set "any" true}}{{/set~}} +                {{~/if~}} +            {{~/each~}} +            {{~#if (get "any")}})</i> {{/if~}} +            {{~/if~}} +        {{~/scope~}} +        {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} +    {{~/unless~}} +{{/inline}} + +{{#*inline "glossary"}} +    {{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries data=.~}} +    {{~> glossary-single definition brief=brief compactGlossaries=../compactGlossaries data=../.~}} +{{/inline}} + +<<<UPDATE-ADDITIONS>>> +{{~> (lookup . "marker") ~}} +`.trimStart() +            }, +            // glossary and glossary-brief update +            { +                oldVersion: 7, +                newVersion: 12, +                old: ` +{{#*inline "glossary-single"}} +    {{~#unless brief~}} +        {{~#scope~}} +            {{~#set "any" false}}{{/set~}} +            {{~#if definitionTags~}}{{#each definitionTags~}} +                {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}} +                    {{~#if (get "any")}}, {{else}}<i>({{/if~}} +                    {{name}} +                    {{~#set "any" true}}{{/set~}} +                {{~/if~}} +            {{~/each~}} +            {{~#if (get "any")}})</i> {{/if~}} +            {{~/if~}} +        {{~/scope~}} +        {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} +    {{~/unless~}} +    {{~#if glossary.[1]~}} +        {{~#if compactGlossaries~}} +            {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} +        {{~else~}} +            <ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul> +        {{~/if~}} +    {{~else~}} +        {{~#multiLine}}{{glossary.[0]}}{{/multiLine~}} +    {{~/if~}} +{{/inline}} + +{{#*inline "character"}} +    {{~definition.character~}} +{{/inline}} + +{{#*inline "glossary"}} +    <div style="text-align: left;"> +    {{~#if modeKanji~}} +        {{~#if definition.glossary.[1]~}} +            <ol>{{#each definition.glossary}}<li>{{.}}</li>{{/each}}</ol> +        {{~else~}} +            {{definition.glossary.[0]}} +        {{~/if~}} +    {{~else~}} +        {{~#if group~}} +            {{~#if definition.definitions.[1]~}} +                <ol>{{#each definition.definitions}}<li>{{> glossary-single brief=../brief compactGlossaries=../compactGlossaries data=../.}}</li>{{/each}}</ol> +            {{~else~}} +                {{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries data=.~}} +            {{~/if~}} +        {{~else if merge~}} +            {{~#if definition.definitions.[1]~}} +                <ol>{{#each definition.definitions}}<li>{{> glossary-single brief=../brief compactGlossaries=../compactGlossaries data=../.}}</li>{{/each}}</ol> +            {{~else~}} +                {{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries data=.~}} +            {{~/if~}} +        {{~else~}} +            {{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries data=.~}} +        {{~/if~}} +    {{~/if~}} +    </div> +{{/inline}} + +{{#*inline "glossary-brief"}} +    {{~> glossary brief=true ~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + +                expected: ` +{{#*inline "glossary-single"}} +    {{~#unless brief~}} +        {{~#scope~}} +            {{~#set "any" false}}{{/set~}} +            {{~#each definitionTags~}} +                {{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}} +                    {{~#if (get "any")}}, {{else}}<i>({{/if~}} +                    {{name}} +                    {{~#set "any" true}}{{/set~}} +                {{~/if~}} +            {{~/each~}} +            {{~#unless noDictionaryTag~}} +                {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}} +                    {{~#if (get "any")}}, {{else}}<i>({{/if~}} +                    {{dictionary}} +                    {{~#set "any" true}}{{/set~}} +                {{~/if~}} +            {{~/unless~}} +            {{~#if (get "any")}})</i> {{/if~}} +        {{~/scope~}} +        {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} +    {{~/unless~}} +    {{~#if (op "<=" glossary.length 1)~}} +        {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} +    {{~else if @root.compactGlossaries~}} +        {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} +    {{~else~}} +        <ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul> +    {{~/if~}} +    {{~#set "previousDictionary" dictionary~}}{{~/set~}} +{{/inline}} + +{{#*inline "character"}} +    {{~definition.character~}} +{{/inline}} + +{{~#*inline "glossary"~}} +    <div style="text-align: left;"> +    {{~#scope~}} +        {{~#if (op "===" definition.type "term")~}} +            {{~> glossary-single definition brief=brief noDictionaryTag=noDictionaryTag ~}} +        {{~else if (op "||" (op "===" definition.type "termGrouped") (op "===" definition.type "termMerged"))~}} +            {{~#if (op ">" definition.definitions.length 1)~}} +                <ol>{{~#each definition.definitions~}}<li>{{~> glossary-single . brief=../brief noDictionaryTag=../noDictionaryTag ~}}</li>{{~/each~}}</ol> +            {{~else~}} +                {{~#each definition.definitions~}}{{~> glossary-single . brief=../brief noDictionaryTag=../noDictionaryTag ~}}{{~/each~}} +            {{~/if~}} +        {{~else if (op "===" definition.type "kanji")~}} +            {{~#if (op ">" definition.glossary.length 1)~}} +                <ol>{{#each definition.glossary}}<li>{{.}}</li>{{/each}}</ol> +            {{~else~}} +                {{~#each definition.glossary~}}{{.}}{{~/each~}} +            {{~/if~}} +        {{~/if~}} +    {{~/scope~}} +    </div> +{{~/inline~}} + +{{#*inline "glossary-no-dictionary"}} +    {{~> glossary noDictionaryTag=true ~}} +{{/inline}} + +{{#*inline "glossary-brief"}} +    {{~> glossary brief=true ~}} +{{/inline}} + +<<<UPDATE-ADDITIONS>>> +{{~> (lookup . "marker") ~}}`.trimStart() +            }, +            // formatGlossary update +            { +                oldVersion: 12, +                newVersion: 13, +                old: ` +{{#*inline "example"}} +    {{~#if (op "<=" glossary.length 1)~}} +        {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} +    {{~else if @root.compactGlossaries~}} +        {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} +    {{~else~}} +        <ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul> +    {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + +                expected: ` +{{#*inline "example"}} +    {{~#if (op "<=" glossary.length 1)~}} +        {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}} +    {{~else if @root.compactGlossaries~}} +        {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}} +    {{~else~}} +        <ul>{{#each glossary}}<li>{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}</li>{{/each}}</ul> +    {{~/if~}} +{{/inline}} + +<<<UPDATE-ADDITIONS>>> +{{~> (lookup . "marker") ~}}`.trimStart() +            }, +            // hasMedia/getMedia update +            { +                oldVersion: 12, +                newVersion: 13, +                old: ` +{{#*inline "audio"}} +    {{~#if definition.audioFileName~}} +        [sound:{{definition.audioFileName}}] +    {{~/if~}} +{{/inline}} + +{{#*inline "screenshot"}} +    <img src="{{definition.screenshotFileName}}" /> +{{/inline}} + +{{#*inline "clipboard-image"}} +    {{~#if definition.clipboardImageFileName~}} +        <img src="{{definition.clipboardImageFileName}}" /> +    {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-text"}} +    {{~#if definition.clipboardText~}}{{definition.clipboardText}}{{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + +                expected: ` +{{#*inline "audio"}} +    {{~#if (hasMedia "audio")~}} +        [sound:{{#getMedia "audio"}}{{/getMedia}}] +    {{~/if~}} +{{/inline}} + +{{#*inline "screenshot"}} +    {{~#if (hasMedia "screenshot")~}} +        <img src="{{#getMedia "screenshot"}}{{/getMedia}}" /> +    {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-image"}} +    {{~#if (hasMedia "clipboardImage")~}} +        <img src="{{#getMedia "clipboardImage"}}{{/getMedia}}" /> +    {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-text"}} +    {{~#if (hasMedia "clipboardText")}}{{#getMedia "clipboardText"}}{{/getMedia}}{{/if~}} +{{/inline}} + +<<<UPDATE-ADDITIONS>>> +{{~> (lookup . "marker") ~}}`.trimStart() +            }, +            // hasMedia/getMedia update +            { +                oldVersion: 12, +                newVersion: 13, +                old: ` +{{! Pitch Accents }} +{{#*inline "pitch-accent-item-downstep-notation"}} +    {{~#scope~}} +        <span> +        {{~#set "style1a"~}}display:inline-block;position:relative;{{~/set~}} +        {{~#set "style1b"~}}padding-right:0.1em;margin-right:0.1em;{{~/set~}} +        {{~#set "style2a"~}}display:block;user-select:none;pointer-events:none;position:absolute;top:0.1em;left:0;right:0;height:0;border-top:0.1em solid;{{~/set~}} +        {{~#set "style2b"~}}right:-0.1em;height:0.4em;border-right:0.1em solid;{{~/set~}} +        {{~#each (getKanaMorae reading)~}} +            {{~#set "style1"}}{{#get "style1a"}}{{/get}}{{/set~}} +            {{~#set "style2"}}{{/set~}} +            {{~#if (isMoraPitchHigh @index ../position)}} +                {{~#set "style2"}}{{#get "style2a"}}{{/get}}{{/set~}} +                {{~#if (op "!" (isMoraPitchHigh (op "+" @index 1) ../position))~}} +                    {{~#set "style1" (op "+" (get "style1") (get "style1b"))}}{{/set~}} +                    {{~#set "style2" (op "+" (get "style2") (get "style2b"))}}{{/set~}} +                {{~/if~}} +            {{~/if~}} +            <span style="{{#get "style1"}}{{/get}}">{{{.}}}<span style="{{#get "style2"}}{{/get}}"></span></span> +        {{~/each~}} +        </span> +    {{~/scope~}} +{{/inline}} + +{{#*inline "pitch-accent-item-graph-position-x"}}{{#op "+" 25 (op "*" index 50)}}{{/op}}{{/inline}} +{{#*inline "pitch-accent-item-graph-position-y"}}{{#op "+" 25 (op "?:" (isMoraPitchHigh index position) 0 50)}}{{/op}}{{/inline}} +{{#*inline "pitch-accent-item-graph-position"}}{{> pitch-accent-item-graph-position-x index=index position=position}} {{> pitch-accent-item-graph-position-y index=index position=position}}{{/inline}} +{{#*inline "pitch-accent-item-graph"}} +    {{~#scope~}} +        {{~#set "morae" (getKanaMorae reading)}}{{/set~}} +        {{~#set "morae-count" (property (get "morae") "length")}}{{/set~}} +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {{#op "+" 50 (op "*" 50 (get "morae-count"))}}{{/op}} 100" style="display:inline-block;height:2em;"> +    <defs> +        <g id="term-pitch-accent-graph-dot"><circle cx="0" cy="0" r="15" style="fill:#000;stroke:#000;stroke-width:5;" /></g> +        <g id="term-pitch-accent-graph-dot-downstep"><circle cx="0" cy="0" r="15" style="fill:none;stroke:#000;stroke-width:5;" /><circle cx="0" cy="0" r="5" style="fill:none;stroke:#000;stroke-width:5;" /></g> +        <g id="term-pitch-accent-graph-triangle"><path d="M0 13 L15 -13 L-15 -13 Z" style="fill:none;stroke:#000;stroke-width:5;" /></g> +    </defs> +    <path style="fill:none;stroke:#000;stroke-width:5;" d=" +    {{~#set "cmd" "M"}}{{/set~}} +    {{~#each (get "morae")~}} +        {{~#get "cmd"}}{{/get~}} +        {{~> pitch-accent-item-graph-position index=@index position=../position~}} +        {{~#set "cmd" "L"}}{{/set~}} +    {{~/each~}} +    "></path> +    <path style="fill:none;stroke:#000;stroke-width:5;stroke-dasharray:5 5;" d="M{{> pitch-accent-item-graph-position index=(op "-" (get "morae-count") 1) position=position}} L{{> pitch-accent-item-graph-position index=(get "morae-count") position=position}}"></path> +    {{#each (get "morae")}} +    <use href="{{#if (op "&&" (isMoraPitchHigh @index ../position) (op "!" (isMoraPitchHigh (op "+" @index 1) ../position)))}}#term-pitch-accent-graph-dot-downstep{{else}}#term-pitch-accent-graph-dot{{/if}}" x="{{> pitch-accent-item-graph-position-x index=@index position=../position}}" y="{{> pitch-accent-item-graph-position-y index=@index position=../position}}"></use> +    {{/each}} +    <use href="#term-pitch-accent-graph-triangle" x="{{> pitch-accent-item-graph-position-x index=(get "morae-count") position=position}}" y="{{> pitch-accent-item-graph-position-y index=(get "morae-count") position=position}}"></use> +</svg> +    {{~/scope~}} +{{/inline}} + +{{#*inline "pitch-accent-item-position"~}} +    <span>[{{position}}]</span> +{{~/inline}} + +{{#*inline "pitch-accent-item"}} +    {{~#if (op "==" format "downstep-notation")~}} +        {{~> pitch-accent-item-downstep-notation~}} +    {{~else if (op "==" format "graph")~}} +        {{~> pitch-accent-item-graph~}} +    {{~else if (op "==" format "position")~}} +        {{~> pitch-accent-item-position~}} +    {{~/if~}} +{{/inline}} + +{{#*inline "pitch-accent-item-disambiguation"}} +    {{~#scope~}} +        {{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} +        {{~#if (op ">" (property (get "exclusive") "length") 0)~}} +            {{~#set "separator" ""~}}{{/set~}} +            <em>({{#each (get "exclusive")~}} +                {{~#get "separator"}}{{/get~}}{{{.}}} +            {{~/each}} only) </em> +        {{~/if~}} +    {{~/scope~}} +{{/inline}} + +{{#*inline "pitch-accent-list"}} +    {{~#if (op ">" pitchCount 0)~}} +        {{~#if (op ">" pitchCount 1)~}}<ol>{{~/if~}} +        {{~#each pitches~}} +            {{~#each pitches~}} +                {{~#if (op ">" ../../pitchCount 1)~}}<li>{{~/if~}} +                    {{~> pitch-accent-item-disambiguation~}} +                    {{~> pitch-accent-item format=../../format~}} +                {{~#if (op ">" ../../pitchCount 1)~}}</li>{{~/if~}} +            {{~/each~}} +        {{~/each~}} +        {{~#if (op ">" pitchCount 1)~}}</ol>{{~/if~}} +    {{~else~}} +        No pitch accent data +    {{~/if~}} +{{/inline}} + +{{#*inline "pitch-accents"}} +    {{~> pitch-accent-list format='downstep-notation'~}} +{{/inline}} + +{{#*inline "pitch-accent-graphs"}} +    {{~> pitch-accent-list format='graph'~}} +{{/inline}} + +{{#*inline "pitch-accent-positions"}} +    {{~> pitch-accent-list format='position'~}} +{{/inline}} +{{! End Pitch Accents }} + +{{~> (lookup . "marker") ~}}`.trimStart(), + +                expected: ` +{{! Pitch Accents }} +{{#*inline "pitch-accent-item"}} +    {{~#pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}{{~/pronunciation~}} +{{/inline}} + +{{#*inline "pitch-accent-item-disambiguation"}} +    {{~#scope~}} +        {{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} +        {{~#if (op ">" (property (get "exclusive") "length") 0)~}} +            {{~#set "separator" ""~}}{{/set~}} +            <em>({{#each (get "exclusive")~}} +                {{~#get "separator"}}{{/get~}}{{{.}}} +            {{~/each}} only) </em> +        {{~/if~}} +    {{~/scope~}} +{{/inline}} + +{{#*inline "pitch-accent-list"}} +    {{~#if (op ">" pitchCount 0)~}} +        {{~#if (op ">" pitchCount 1)~}}<ol>{{~/if~}} +        {{~#each pitches~}} +            {{~#each pitches~}} +                {{~#if (op ">" ../../pitchCount 1)~}}<li>{{~/if~}} +                    {{~> pitch-accent-item-disambiguation~}} +                    {{~> pitch-accent-item format=../../format~}} +                {{~#if (op ">" ../../pitchCount 1)~}}</li>{{~/if~}} +            {{~/each~}} +        {{~/each~}} +        {{~#if (op ">" pitchCount 1)~}}</ol>{{~/if~}} +    {{~else~}} +        No pitch accent data +    {{~/if~}} +{{/inline}} + +{{#*inline "pitch-accents"}} +    {{~> pitch-accent-list format='text'~}} +{{/inline}} + +{{#*inline "pitch-accent-graphs"}} +    {{~> pitch-accent-list format='graph'~}} +{{/inline}} + +{{#*inline "pitch-accent-positions"}} +    {{~> pitch-accent-list format='position'~}} +{{/inline}} +{{! End Pitch Accents }} + +<<<UPDATE-ADDITIONS>>> +{{~> (lookup . "marker") ~}}`.trimStart() +            }, +            // block helper update: furigana and furiganaPlain +            { +                oldVersion: 20, +                newVersion: 21, +                old: ` +{{#*inline "furigana"}} +    {{~#if merge~}} +        {{~#each definition.expressions~}} +            <span class="expression-{{termFrequency}}">{{~#furigana}}{{{.}}}{{/furigana~}}</span> +            {{~#unless @last}}、{{/unless~}} +        {{~/each~}} +    {{~else~}} +        {{#furigana}}{{{definition}}}{{/furigana}} +    {{~/if~}} +{{/inline}} + +{{#*inline "furigana-plain"}} +    {{~#if merge~}} +        {{~#each definition.expressions~}} +            <span class="expression-{{termFrequency}}">{{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}}</span> +            {{~#unless @last}}、{{/unless~}} +        {{~/each~}} +    {{~else~}} +        {{#furiganaPlain}}{{{definition}}}{{/furiganaPlain}} +    {{~/if~}} +{{/inline}} + +{{#*inline "frequencies"}} +    {{~#if (op ">" definition.frequencies.length 0)~}} +        <ul style="text-align: left;"> +        {{~#each definition.frequencies~}} +            <li> +            {{~#if (op "!==" ../definition.type "kanji")~}} +                {{~#if (op "||" (op ">" ../uniqueExpressions.length 1) (op ">" ../uniqueReadings.length 1))~}}( +                    {{~#furigana expression reading~}}{{~/furigana~}} +                ) {{/if~}} +            {{~/if~}} +            {{~dictionary}}: {{frequency~}} +            </li> +        {{~/each~}} +        </ul> +    {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + +                expected: ` +{{#*inline "furigana"}} +    {{~#if merge~}} +        {{~#each definition.expressions~}} +            <span class="expression-{{termFrequency}}">{{~furigana .~}}</span> +            {{~#unless @last}}、{{/unless~}} +        {{~/each~}} +    {{~else~}} +        {{furigana definition}} +    {{~/if~}} +{{/inline}} + +{{#*inline "furigana-plain"}} +    {{~#if merge~}} +        {{~#each definition.expressions~}} +            <span class="expression-{{termFrequency}}">{{~furiganaPlain .~}}</span> +            {{~#unless @last}}、{{/unless~}} +        {{~/each~}} +    {{~else~}} +        {{furiganaPlain definition}} +    {{~/if~}} +{{/inline}} + +{{#*inline "frequencies"}} +    {{~#if (op ">" definition.frequencies.length 0)~}} +        <ul style="text-align: left;"> +        {{~#each definition.frequencies~}} +            <li> +            {{~#if (op "!==" ../definition.type "kanji")~}} +                {{~#if (op "||" (op ">" ../uniqueExpressions.length 1) (op ">" ../uniqueReadings.length 1))~}}( +                    {{~furigana expression reading~}} +                ) {{/if~}} +            {{~/if~}} +            {{~dictionary}}: {{frequency~}} +            </li> +        {{~/each~}} +        </ul> +    {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() +            }, +            // block helper update: formatGlossary +            { +                oldVersion: 20, +                newVersion: 21, +                old: ` +{{#*inline "glossary-single"}} +    {{~#unless brief~}} +        {{~#scope~}} +            {{~#set "any" false}}{{/set~}} +            {{~#each definitionTags~}} +                {{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}} +                    {{~#if (get "any")}}, {{else}}<i>({{/if~}} +                    {{name}} +                    {{~#set "any" true}}{{/set~}} +                {{~/if~}} +            {{~/each~}} +            {{~#unless noDictionaryTag~}} +                {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}} +                    {{~#if (get "any")}}, {{else}}<i>({{/if~}} +                    {{dictionary}} +                    {{~#set "any" true}}{{/set~}} +                {{~/if~}} +            {{~/unless~}} +            {{~#if (get "any")}})</i> {{/if~}} +        {{~/scope~}} +        {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} +    {{~/unless~}} +    {{~#if (op "<=" glossary.length 1)~}} +        {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}} +    {{~else if @root.compactGlossaries~}} +        {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}} +    {{~else~}} +        <ul>{{#each glossary}}<li>{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}</li>{{/each}}</ul> +    {{~/if~}} +    {{~#set "previousDictionary" dictionary~}}{{~/set~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + +                expected: ` +{{#*inline "glossary-single"}} +    {{~#unless brief~}} +        {{~#scope~}} +            {{~set "any" false~}} +            {{~#each definitionTags~}} +                {{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}} +                    {{~#if (get "any")}}, {{else}}<i>({{/if~}} +                    {{name}} +                    {{~set "any" true~}} +                {{~/if~}} +            {{~/each~}} +            {{~#unless noDictionaryTag~}} +                {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}} +                    {{~#if (get "any")}}, {{else}}<i>({{/if~}} +                    {{dictionary}} +                    {{~set "any" true~}} +                {{~/if~}} +            {{~/unless~}} +            {{~#if (get "any")}})</i> {{/if~}} +        {{~/scope~}} +        {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} +    {{~/unless~}} +    {{~#if (op "<=" glossary.length 1)~}} +        {{#each glossary}}{{formatGlossary ../dictionary .}}{{/each}} +    {{~else if @root.compactGlossaries~}} +        {{#each glossary}}{{formatGlossary ../dictionary .}}{{#unless @last}} | {{/unless}}{{/each}} +    {{~else~}} +        <ul>{{#each glossary}}<li>{{formatGlossary ../dictionary .}}</li>{{/each}}</ul> +    {{~/if~}} +    {{~set "previousDictionary" dictionary~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() +            }, +            // block helper update: set and get +            { +                oldVersion: 20, +                newVersion: 21, +                old: ` +{{#*inline "pitch-accent-item-disambiguation"}} +    {{~#scope~}} +        {{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} +        {{~#if (op ">" (property (get "exclusive") "length") 0)~}} +            {{~#set "separator" ""~}}{{/set~}} +            <em>({{#each (get "exclusive")~}} +                {{~#get "separator"}}{{/get~}}{{{.}}} +            {{~/each}} only) </em> +        {{~/if~}} +    {{~/scope~}} +{{/inline}} + +{{#*inline "stroke-count"}} +    {{~#scope~}} +        {{~#set "found" false}}{{/set~}} +        {{~#each definition.stats.misc~}} +            {{~#if (op "===" name "strokes")~}} +                {{~#set "found" true}}{{/set~}} +                Stroke count: {{value}} +            {{~/if~}} +        {{~/each~}} +        {{~#if (op "!" (get "found"))~}} +            Stroke count: Unknown +        {{~/if~}} +    {{~/scope~}} +{{/inline}} + +{{#*inline "part-of-speech"}} +    {{~#scope~}} +        {{~#if (op "!==" definition.type "kanji")~}} +            {{~#set "first" true}}{{/set~}} +            {{~#each definition.expressions~}} +                {{~#each wordClasses~}} +                    {{~#unless (get (concat "used_" .))~}} +                        {{~> part-of-speech-pretty . ~}} +                        {{~#unless (get "first")}}, {{/unless~}} +                        {{~#set (concat "used_" .) true~}}{{~/set~}} +                        {{~#set "first" false~}}{{~/set~}} +                    {{~/unless~}} +                {{~/each~}} +            {{~/each~}} +            {{~#if (get "first")~}}Unknown{{~/if~}} +        {{~/if~}} +    {{~/scope~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + +                expected: ` +{{#*inline "pitch-accent-item-disambiguation"}} +    {{~#scope~}} +        {{~set "exclusive" (spread exclusiveExpressions exclusiveReadings)~}} +        {{~#if (op ">" (property (get "exclusive") "length") 0)~}} +            {{~set "separator" ""~}} +            <em>({{#each (get "exclusive")~}} +                {{~get "separator"~}}{{{.}}} +            {{~/each}} only) </em> +        {{~/if~}} +    {{~/scope~}} +{{/inline}} + +{{#*inline "stroke-count"}} +    {{~#scope~}} +        {{~set "found" false~}} +        {{~#each definition.stats.misc~}} +            {{~#if (op "===" name "strokes")~}} +                {{~set "found" true~}} +                Stroke count: {{value}} +            {{~/if~}} +        {{~/each~}} +        {{~#if (op "!" (get "found"))~}} +            Stroke count: Unknown +        {{~/if~}} +    {{~/scope~}} +{{/inline}} + +{{#*inline "part-of-speech"}} +    {{~#scope~}} +        {{~#if (op "!==" definition.type "kanji")~}} +            {{~set "first" true~}} +            {{~#each definition.expressions~}} +                {{~#each wordClasses~}} +                    {{~#unless (get (concat "used_" .))~}} +                        {{~> part-of-speech-pretty . ~}} +                        {{~#unless (get "first")}}, {{/unless~}} +                        {{~set (concat "used_" .) true~}} +                        {{~set "first" false~}} +                    {{~/unless~}} +                {{~/each~}} +            {{~/each~}} +            {{~#if (get "first")~}}Unknown{{~/if~}} +        {{~/if~}} +    {{~/scope~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() +            }, +            // block helper update: hasMedia and getMedia +            { +                oldVersion: 20, +                newVersion: 21, +                old: ` +{{#*inline "audio"}} +    {{~#if (hasMedia "audio")~}} +        [sound:{{#getMedia "audio"}}{{/getMedia}}] +    {{~/if~}} +{{/inline}} + +{{#*inline "screenshot"}} +    {{~#if (hasMedia "screenshot")~}} +        <img src="{{#getMedia "screenshot"}}{{/getMedia}}" /> +    {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-image"}} +    {{~#if (hasMedia "clipboardImage")~}} +        <img src="{{#getMedia "clipboardImage"}}{{/getMedia}}" /> +    {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-text"}} +    {{~#if (hasMedia "clipboardText")}}{{#getMedia "clipboardText"}}{{/getMedia}}{{/if~}} +{{/inline}} + +{{#*inline "selection-text"}} +    {{~#if (hasMedia "selectionText")}}{{#getMedia "selectionText"}}{{/getMedia}}{{/if~}} +{{/inline}} + +{{#*inline "sentence-furigana"}} +    {{~#if definition.cloze~}} +        {{~#if (hasMedia "textFurigana" definition.cloze.sentence)~}} +            {{#getMedia "textFurigana" definition.cloze.sentence escape=false}}{{/getMedia}} +        {{~else~}} +            {{definition.cloze.sentence}} +        {{~/if~}} +    {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + +                expected: ` +{{#*inline "audio"}} +    {{~#if (hasMedia "audio")~}} +        [sound:{{getMedia "audio"}}] +    {{~/if~}} +{{/inline}} + +{{#*inline "screenshot"}} +    {{~#if (hasMedia "screenshot")~}} +        <img src="{{getMedia "screenshot"}}" /> +    {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-image"}} +    {{~#if (hasMedia "clipboardImage")~}} +        <img src="{{getMedia "clipboardImage"}}" /> +    {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-text"}} +    {{~#if (hasMedia "clipboardText")}}{{getMedia "clipboardText"}}{{/if~}} +{{/inline}} + +{{#*inline "selection-text"}} +    {{~#if (hasMedia "selectionText")}}{{getMedia "selectionText"}}{{/if~}} +{{/inline}} + +{{#*inline "sentence-furigana"}} +    {{~#if definition.cloze~}} +        {{~#if (hasMedia "textFurigana" definition.cloze.sentence)~}} +            {{getMedia "textFurigana" definition.cloze.sentence escape=false}} +        {{~else~}} +            {{definition.cloze.sentence}} +        {{~/if~}} +    {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() +            }, +            // block helper update: pronunciation +            { +                oldVersion: 20, +                newVersion: 21, +                old: ` +{{#*inline "pitch-accent-item"}} +    {{~#pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}{{~/pronunciation~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + +                expected: ` +{{#*inline "pitch-accent-item"}} +    {{~pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() +            } +        ]; + +        const updatesPattern = /<<<UPDATE-ADDITIONS>>>/g; +        for (const {old, expected, oldVersion, newVersion} of data) { +            const options = createOptionsTestData1(); +            options.profiles[0].options.anki.fieldTemplates = old; +            options.version = oldVersion; + +            const expected2 = expected.replace(updatesPattern, getUpdateAdditions(oldVersion, newVersion)); + +            const optionsUpdated = structuredClone(await optionsUtil.update(options, newVersion)); +            const fieldTemplatesActual = optionsUpdated.profiles[0].options.anki.fieldTemplates; +            expect(fieldTemplatesActual).toStrictEqual(expected2); +        } +    }); +} + + +async function main() { +    await testUpdate(); +    await testDefault(); +    await testFieldTemplatesUpdate(); +} + +await main();  |