diff --git a/.github/workflows/update-browserlist.yml b/.github/workflows/update-browserlist.yml index 5ad70a9f5..be811d6d4 100644 --- a/.github/workflows/update-browserlist.yml +++ b/.github/workflows/update-browserlist.yml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v2 - run: npx browserslist@latest --update-db - name: Commit changes - uses: EndBug/add-and-commit@v7 + uses: EndBug/add-and-commit@v8 with: author_name: ${{ github.actor }} author_email: ${{ github.actor }}@users.noreply.github.com diff --git a/.github/workflows/update-monaco-editor.yml b/.github/workflows/update-monaco-editor.yml index 394fd4b24..045828424 100644 --- a/.github/workflows/update-monaco-editor.yml +++ b/.github/workflows/update-monaco-editor.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v2 - run: ./bin/update-monaco - name: Commit changes - uses: EndBug/add-and-commit@v7 + uses: EndBug/add-and-commit@v8 with: author_name: ${{ github.actor }} author_email: ${{ github.actor }}@users.noreply.github.com diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..cd3023f66 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.formatOnSave": true, + "cSpell.words": ["pako", "Serde", "serdes"] +} diff --git a/Dockerfile b/Dockerfile index 686d37939..2b07d510f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ # Stop : press ctrl + c # or # docker stop mermaid-live-editor -FROM node:17.3.1 as mermaid-live-editor-builder +FROM node:17.4.0 as mermaid-live-editor-builder COPY --chown=node:node . /home WORKDIR /home RUN yarn install diff --git a/cypress/integration/loadSite.spec.ts b/cypress/integration/loadSite.spec.ts index c8962b2d1..aa939cc4b 100644 --- a/cypress/integration/loadSite.spec.ts +++ b/cypress/integration/loadSite.spec.ts @@ -93,4 +93,31 @@ describe('Site Loads', () => { cy.visit('/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAg'); cy.contains('Please Click here to Raise an issue in github.'); }); + + it('should load uncompressed URL', () => { + cy.visit( + '/edit/#eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW05ldyBZZWFyXSAtLT58R2V0IG1vbmV5fCBCKEdvIHNob3BwaW5nKVxuICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfVxuICAgIEMgLS0-fE9uZXwgRFtMYXB0b3BdXG4gICAgQyAtLT58VHdvfCBFW2lQaG9uZV1cbiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXSIsIm1lcm1haWQiOiJ7XG4gIFwidGhlbWVcIjogXCJkZWZhdWx0XCJcbn0iLCJ1cGRhdGVFZGl0b3IiOmZhbHNlLCJhdXRvU3luYyI6dHJ1ZSwidXBkYXRlRGlhZ3JhbSI6ZmFsc2V9' + ); + cy.contains('New Year'); + cy.visit( + '/edit#eyJjb2RlIjoiY2xhc3NEaWFncmFtXG4gICAgQW5pbWFsIDx8LS0gRHVja1xuICAgIEFuaW1hbCA8fC0tIEZpc2hcbiAgICBBbmltYWwgPHwtLSBaZWJyYVxuICAgIEFuaW1hbCA6ICtpbnQgYWdlXG4gICAgQW5pbWFsIDogK1N0cmluZyBnZW5kZXJcbiAgICBBbmltYWw6ICtpc01hbW1hbCgpXG4gICAgQW5pbWFsOiArbWF0ZSgpXG4gICAgY2xhc3MgRHVja3tcbiAgICAgICtTdHJpbmcgYmVha0NvbG9yXG4gICAgICArc3dpbSgpXG4gICAgICArcXVhY2soKVxuICAgIH1cbiAgICBjbGFzcyBGaXNoe1xuICAgICAgLWludCBzaXplSW5GZWV0XG4gICAgICAtY2FuRWF0KClcbiAgICB9XG4gICAgY2xhc3MgWmVicmF7XG4gICAgICArYm9vbCBpc193aWxkXG4gICAgICArcnVuKClcbiAgICB9XG4gICAgICAgICAgICAiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGFya1wiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ' + ); + cy.contains('Animal'); + cy.visit( + '/edit/#base64:eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW05ldyBZZWFyXSAtLT58R2V0IG1vbmV5fCBCKEdvIHNob3BwaW5nKVxuICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfVxuICAgIEMgLS0-fE9uZXwgRFtMYXB0b3BdXG4gICAgQyAtLT58VHdvfCBFW2lQaG9uZV1cbiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXSIsIm1lcm1haWQiOiJ7XG4gIFwidGhlbWVcIjogXCJkZWZhdWx0XCJcbn0iLCJ1cGRhdGVFZGl0b3IiOmZhbHNlLCJhdXRvU3luYyI6dHJ1ZSwidXBkYXRlRGlhZ3JhbSI6ZmFsc2V9' + ); + cy.contains('New Year'); + }); + + it('should load compressed URL', () => { + cy.visit( + '/edit#pako:eNpVkM2KwkAQhF-l6dMK5gVyEDRxvYi7sF6WjIcm0zqDzg_jBJEk725Hd2G3Tw31VVFUj23QjCWeEkUD-1p5kFs2O77BN1M6QFEshg1ncMHzfYDV2ybA1YQYrT_NXvhqgqDqtxPGkI315_ElVU__h-cB6mZLMYd4-Kvsb2GAdWM_jcT_V0xicb03RyqPVLSUoJI-OEfHyZHV0rqfDAqzYccKS3k1pbNC5Ufhuqgp81rbHBJKxuXKc6Quh6-7b7HMqeNfqLYkC7gfanwAlW1ZvQ' + ); + cy.contains('New Year'); + cy.visit( + '/edit#pako:eNptkU1PwzAMhv9K5BOI9Q9EXBDbJA477YYqITcxndV8QD40weh_Jy1rGR0-OY_tV2_sEyivCSQogzGuGduAtnaixINji0bcf1WVWGfVXdMtx8M1faYm4B8sxR27JLClJd6nwK4VLTlN4bI4jMQd2pLe3C4KFhNNcLQ92jv9ADGLNoTdozc-zIV4ZDsNlud7RtVN7_5Sb_jYrFcN3iN_0pPbEqUZK3QbTP_Ojyv4NdR4bwTHlyMbPcOQ3WJ2CliBpWCRdbnLqFJDOpClGmRJNYauhtr1pS-_6bKMjebkA8hXNJFWgDn5_YdTIFPINDWdb3vu6r8BaWOZRQ' + ); + cy.reload(); + cy.contains('Animal'); + }); }); diff --git a/cypress/snapshots.js b/cypress/snapshots.js index 1abcb9154..d023001fe 100644 --- a/cypress/snapshots.js +++ b/cypress/snapshots.js @@ -16,7 +16,7 @@ module.exports = { "1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping!!)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello world\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"files\",\"config\":{\"codeURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd\",\"configURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json\"}}}" } }, - "__version": "9.1.1", + "__version": "9.2.0", "Auto sync tests": { "should dim diagram when code is edited": { "1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n C --> Test\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":false,\"autoSync\":false,\"updateDiagram\":false}" diff --git a/package.json b/package.json index d2d563949..6ed1a6e02 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,9 @@ "devDependencies": { "@cypress/snapshot": "^2.1.7", "@sveltejs/adapter-static": "1.0.0-next.26", - "@sveltejs/kit": "1.0.0-next.232", + "@sveltejs/kit": "1.0.0-next.236", "@types/mermaid": "^8.2.7", + "@types/pako": "^1.0.3", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "autoprefixer": "^10.4.2", @@ -34,7 +35,7 @@ "eslint-plugin-svelte3": "^3.4.0", "eslint-plugin-tailwindcss": "^3.3.0", "husky": "^7.0.4", - "lint-staged": "^12.2.1", + "lint-staged": "^12.2.2", "mocha": "^9.1.4", "node-html-parser": "^5.2.0", "postcss": "^8.4.5", @@ -45,7 +46,7 @@ "svelte-preprocess": "^4.10.2", "tailwindcss": "^3.0.13", "tslib": "^2.3.1", - "typescript": "^4.5.4" + "typescript": "^4.5.5" }, "type": "module", "dependencies": { @@ -57,6 +58,7 @@ "mermaid": "8.13.9", "moment": "^2.29.1", "monaco-editor": "^0.31.1", + "monaco-mermaid": "^1.0.2", "pako": "2.0.4", "random-word-slugs": "^0.1.6" }, diff --git a/src/hooks.ts b/src/hooks.ts index eff8dfade..6eba0ddab 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,7 +1,7 @@ import type { Handle } from '@sveltejs/kit/types/hooks'; -export const handle: Handle = async ({ request, resolve }) => { - const response = await resolve(request, { +export const handle: Handle = async ({ event, resolve }) => { + const response = await resolve(event, { ssr: false }); diff --git a/src/lib/components/actions.svelte b/src/lib/components/actions.svelte index b1b915eaa..b3c411fa7 100644 --- a/src/lib/components/actions.svelte +++ b/src/lib/components/actions.svelte @@ -2,11 +2,11 @@ import { browser } from '$app/env'; import Card from '$lib/components/card/card.svelte'; - import { rendererUrl, krokiRendererUrl } from '$lib/util/env'; - import { base64State, codeStore } from '$lib/util/state'; - import { toBase64, btoa as jsbtoa } from 'js-base64'; + import { krokiRendererUrl, rendererUrl } from '$lib/util/env'; + import { pakoSerde } from '$lib/util/serde'; + import { serializedState, codeStore } from '$lib/util/state'; + import { toBase64 } from 'js-base64'; import moment from 'moment'; - import pako from 'pako'; type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void; @@ -18,7 +18,7 @@ } const svgString = svg.outerHTML .replaceAll('
', '
') - .replaceAll(/]*)>/g, (m, g) => ``); + .replaceAll(/]*)>/g, (m, g: string) => ``); return toBase64(svgString); }; @@ -79,7 +79,7 @@ canvas.toBlob((blob) => { try { // @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1004/files - navigator.clipboard.write([ + void navigator.clipboard.write([ /* eslint-disable no-undef */ // @ts-ignore: https://github.com/microsoft/TypeScript/issues/43821 new ClipboardItem({ @@ -109,7 +109,7 @@ }; const onCopyMarkdown = () => { - document.getElementById('markdown').select(); + (document.getElementById('markdown') as HTMLInputElement).select(); document.execCommand('Copy'); }; @@ -128,33 +128,9 @@ window.location.href = `${window.location.pathname}?gist=${gistURL}`; }; - const textEncode = (str) => { - if (window.TextEncoder) { - return new TextEncoder('utf-8').encode(str); - } - let utf8 = unescape(encodeURIComponent(str)); - let result = new Uint8Array(utf8.length); - for (let i = 0; i < utf8.length; i++) { - result[i] = utf8.charCodeAt(i); - } - return result; - }; - - const onKrokiClick = () => { - const krokiCode = getKrokiCode($codeStore.code); - const krokiUrl = `${krokiRendererUrl}/mermaid/svg/${krokiCode}`; - return window.open(krokiUrl, '_blank'); - }; - - const getKrokiCode = (source) => { - const data = textEncode(source); - const compressed = pako.deflate(data, { level: 9, to: 'string' }); - let result = jsbtoa(compressed).replace(/\+/g, '-').replace(/\//g, '_'); - return result; - }; - let iUrl: string; let svgUrl: string; + let krokiUrl: string; let mdCode: string; let imagemodeselected = 'auto'; let userimagesize = 1080; @@ -163,14 +139,10 @@ if (browser && ['mermaid.live', 'netlify'].some((path) => window.location.host.includes(path))) { isNetlify = true; } - base64State.subscribe((encodedState: string) => { - const stateCopy = JSON.parse(JSON.stringify($codeStore)); - if (typeof stateCopy.mermaid === 'string') { - stateCopy.mermaid = JSON.parse(stateCopy.mermaid); - } - const b64Code = toBase64(JSON.stringify(stateCopy), true); - iUrl = `${rendererUrl}/img/${b64Code}`; - svgUrl = `${rendererUrl}/svg/${b64Code}`; + serializedState.subscribe((encodedState: string) => { + iUrl = `${rendererUrl}/img/${encodedState}`; + svgUrl = `${rendererUrl}/svg/${encodedState}`; + krokiUrl = `${krokiRendererUrl}/mermaid/svg/${pakoSerde.serialize($codeStore.code)}`; mdCode = `[![](${iUrl})](${window.location.protocol}//${window.location.host}${window.location.pathname}#${encodedState})`; }); @@ -194,8 +166,8 @@ -
diff --git a/src/lib/components/editor/editor.svelte b/src/lib/components/editor.svelte similarity index 98% rename from src/lib/components/editor/editor.svelte rename to src/lib/components/editor.svelte index 949ad6546..536bb427a 100644 --- a/src/lib/components/editor/editor.svelte +++ b/src/lib/components/editor.svelte @@ -4,7 +4,7 @@ import { themeStore } from '$lib/util/theme'; import type monaco from 'monaco-editor'; import { createEventDispatcher, onMount } from 'svelte'; - import { initEditor } from './util'; + import initEditor from 'monaco-mermaid'; let divEl: HTMLDivElement = null; let editor: monaco.editor.IStandaloneCodeEditor; diff --git a/src/lib/components/editor/util.ts b/src/lib/components/editor/util.ts deleted file mode 100644 index c6a652d64..000000000 --- a/src/lib/components/editor/util.ts +++ /dev/null @@ -1,552 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export const initEditor = (monacoEditor): void => { - monacoEditor.languages.register({ id: 'mermaid' }); - - const requirementDiagrams = [ - 'requirement', - 'functionalRequirement', - 'interfaceRequirement', - 'performanceRequirement', - 'physicalRequirement', - 'designConstraint' - ]; - - const keywords: { - [key: string]: { - typeKeywords: string[]; - blockKeywords: string[]; - keywords: string[]; - }; - } = { - flowchart: { - typeKeywords: ['flowchart', 'flowchart-v2', 'graph'], - blockKeywords: ['subgraph', 'end'], - keywords: [ - 'TB', - 'TD', - 'BT', - 'RL', - 'LR', - 'click', - 'call', - 'href', - '_self', - '_blank', - '_parent', - '_top', - 'linkStyle', - 'style', - 'classDef', - 'class', - 'direction', - 'interpolate' - ] - }, - sequenceDiagram: { - typeKeywords: ['sequenceDiagram'], - blockKeywords: ['alt', 'par', 'and', 'loop', 'else', 'end', 'rect', 'opt', 'alt', 'rect'], - keywords: [ - 'participant', - 'as', - 'Note', - 'note', - 'right of', - 'left of', - 'over', - 'activate', - 'deactivate', - 'autonumber', - 'title', - 'actor' - ] - }, - classDiagram: { - typeKeywords: ['classDiagram', 'classDiagram-v2'], - blockKeywords: ['class'], - keywords: [ - 'link', - 'click', - 'callback', - 'call', - 'href', - 'cssClass', - 'direction', - 'TB', - 'BT', - 'RL', - 'LR' - ] - }, - stateDiagram: { - typeKeywords: ['stateDiagram', 'stateDiagram-v2'], - blockKeywords: ['state', 'note', 'end'], - keywords: ['state', 'as', 'hide empty description', 'direction', 'TB', 'BT', 'RL', 'LR'] - }, - erDiagram: { - typeKeywords: ['erDiagram'], - blockKeywords: [], - keywords: [] - }, - journey: { - typeKeywords: ['journey'], - blockKeywords: ['section'], - keywords: ['title'] - }, - info: { - typeKeywords: ['info'], - blockKeywords: [], - keywords: ['showInfo'] - }, - gantt: { - typeKeywords: ['gantt'], - blockKeywords: [], - keywords: [ - 'title', - 'dateFormat', - 'axisFormat', - 'todayMarker', - 'section', - 'excludes', - 'inclusiveEndDates' - ] - }, - requirementDiagram: { - typeKeywords: ['requirement', 'requirementDiagram'], - blockKeywords: requirementDiagrams.concat('element'), - keywords: [] - }, - gitGraph: { - typeKeywords: ['gitGraph'], - blockKeywords: [], - keywords: ['commit', 'branch', 'merge', 'reset', 'checkout', 'LR', 'BT'] - }, - pie: { - typeKeywords: ['pie'], - blockKeywords: [], - keywords: ['title', 'showData'] - } - }; - - // Register a tokens provider for the mermaid language - monacoEditor.languages.setMonarchTokensProvider('mermaid', { - ...Object.entries(keywords) - .map((entry) => - Object.fromEntries( - Object.entries(entry[1]).map((deepEntry) => [ - entry[0] + deepEntry[0][0].toUpperCase() + deepEntry[0].slice(1), - deepEntry[1] - ]) - ) - ) - .reduce((overallKeywords, nextKeyword) => ({ ...overallKeywords, ...nextKeyword }), {}), - tokenizer: { - root: [ - [/%%(?=.*%%$)/, { token: 'string', nextEmbedded: 'json' }], - [/%%$/, { token: 'string', nextEmbedded: '@pop' }], - [/^\s*gitGraph/m, 'typeKeyword', 'gitGraph'], - [/^\s*info/m, 'typeKeyword', 'info'], - [/^\s*pie/m, 'typeKeyword', 'pie'], - [/^\s*(flowchart|flowchart-v2|graph)/m, 'typeKeyword', 'flowchart'], - [/^\s*sequenceDiagram/, 'typeKeyword', 'sequenceDiagram'], - [/^\s*classDiagram(-v2)?/, 'typeKeyword', 'classDiagram'], - [/^\s*journey/, 'typeKeyword', 'journey'], - [/^\s*gantt/, 'typeKeyword', 'gantt'], - [/^\s*stateDiagram(-v2)?/, 'typeKeyword', 'stateDiagram'], - [/^\s*erDiagram/, 'typeKeyword', 'erDiagram'], - [/^\s*requirement(Diagram)?/, 'typeKeyword', 'requirementDiagram'], - [/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'] - ], - gitGraph: [ - [/option(?=s)/, { token: 'typeKeyword', next: 'optionsGitGraph' }], - [/(^\s*branch|reset|merge|checkout)(.*$)/, ['keyword', 'variable']], - [ - /[a-zA-Z][\w$]*/, - { - cases: { - '@gitGraphBlockKeywords': 'typeKeyword', - '@gitGraphKeywords': 'keyword' - } - } - ], - [/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'], - [/".*?"/, 'string'], - [/\^/, 'delimiter.bracket'] - ], - optionsGitGraph: [ - [/s$/, { token: 'typeKeyword', nextEmbedded: 'json', matchOnlyAtLineStart: false }], - ['end', { token: 'typeKeyword', next: '@pop', nextEmbedded: '@pop' }] - ], - info: [ - [ - /[a-zA-Z][\w$]*/, - { - cases: { - '@infoBlockKeywords': 'typeKeyword', - '@infoKeywords': 'keyword' - } - } - ] - ], - pie: [ - [/(title)(.*$)/, ['keyword', 'string']], - [ - /[a-zA-Z][\w$]*/, - { - cases: { - '@pieBlockKeywords': 'typeKeyword', - '@pieKeywords': 'keyword' - } - } - ], - [/".*?"/, 'string'], - [/\s*\d+/, 'number'], - [/:/, 'delimiter.bracket'], - [/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'] - ], - flowchart: [ - [/[ox]?(--+|==+)[ox]/, 'transition'], - [ - /[a-zA-Z][\w$]*/, - { - cases: { - '@flowchartBlockKeywords': 'typeKeyword', - '@flowchartKeywords': 'keyword', - '@default': 'variable' - } - } - ], - [/\|+.+?\|+/, 'string'], - [/\[+(\\.+?[\\/]|\/.+?[/\\])\]+/, 'string'], - [/[[>]+[^\]|[]+?\]+/, 'string'], - [/{+.+?}+/, 'string'], - [/\(+.+?\)+/, 'string'], - [/-\.+->?/, 'transition'], - [/(-[-.])([^->]+?)(-{3,}|-{2,}>|\.-+>)/, ['transition', 'string', 'transition']], - [/(==+)([^=]+?)(={3,}|={2,}>)/, ['transition', 'string', 'transition']], - [/|===+|---+/, 'transition'], - [/:::/, 'transition'], - [/[;&]/, 'delimiter.bracket'], - [/".*?"/, 'string'], - [/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'] - ], - sequenceDiagram: [ - [ - /[a-zA-Z][\w$]*/, - { - cases: { - '@sequenceDiagramBlockKeywords': 'typeKeyword', - '@sequenceDiagramKeywords': 'keyword', - '@default': 'variable' - } - } - ], - [/(--?>?>|--?[)x])[+-]?/, 'transition'], - [/(:)([^:\n]*?$)/, ['delimiter.bracket', 'string']], - [/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'] - ], - classDiagram: [ - [/(?!class\s)([a-zA-Z]+)(\s+[a-zA-Z]+)/, ['type', 'variable']], - [/(\*|<\|?|o)?(--|\.\.)(\*|\|?>|o)?/, 'transition'], - [/^\s*class\s(?!.*\{)/, 'keyword'], - [ - /[a-zA-Z][\w$]*/, - { - cases: { - '@classDiagramBlockKeywords': 'typeKeyword', - '@classDiagramKeywords': 'keyword', - '@default': 'variable' - } - } - ], - [/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'], - [/(<<)(.+?)(>>)/, ['delimiter.bracket', 'annotation', 'delimiter.bracket']], - [/".*?"/, 'string'], - [/:::/, 'transition'], - [/:|\+|-|#|~|\*\s*$|\$\s*$|\(|\)|{|}/, 'delimiter.bracket'] - ], - journey: [ - [/(title)(.*)/, ['keyword', 'string']], - [/(section)(.*)/, ['typeKeyword', 'string']], - [ - /[a-zA-Z][\w$]*/, - { - cases: { - '@journeyBlockKeywords': 'typeKeyword', - '@journeyKeywords': 'keyword', - '@default': 'variable' - } - } - ], - [ - /(^\s*.+?)(:)(.*?)(:)(.*?)([,$])/, - [ - 'string', - 'delimiter.bracket', - 'number', - 'delimiter.bracket', - 'variable', - 'delimiter.bracket' - ] - ], - [/,/, 'delimiter.bracket'], - [/(^\s*.+?)(:)([^:]*?)$/, ['string', 'delimiter.bracket', 'variable']], - [/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'] - ], - gantt: [ - [/(title)(.*)/, ['keyword', 'string']], - [/(section)(.*)/, ['typeKeyword', 'string']], - [ - /[a-zA-Z][\w$]*/, - { - cases: { - '@ganttBlockKeywords': 'typeKeyword', - '@ganttKeywords': 'keyword' - } - } - ], - [/(^\s*.*?)(:)/, ['string', 'delimiter.bracket']], - [/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'], - [/:/, 'delimiter.bracket'] - ], - stateDiagram: [ - [/note[^:]*$/, { token: 'typeKeyword', next: 'stateDiagramNote' }], - ['hide empty description', 'keyword'], - [/^\s*state\s(?!.*\{)/, 'keyword'], - [/(<<)(fork|join|choice)(>>)/, 'annotation'], - [/(\[\[)(fork|join|choice)(]])/, ['delimiter.bracket', 'annotation', 'delimiter.bracket']], - [ - /[a-zA-Z][\w$]*/, - { - cases: { - '@stateDiagramBlockKeywords': 'typeKeyword', - '@stateDiagramKeywords': 'keyword', - '@default': 'variable' - } - } - ], - [/".*?"/, 'string'], - [/(:)([^:\n]*?$)/, ['delimiter.bracket', 'string']], - [/{|}/, 'delimiter.bracket'], - [/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'], - [/-->/, 'transition'], - [/\[.*?]/, 'string'] - ], - stateDiagramNote: [ - [/^\s*end note$/, { token: 'typeKeyword', next: '@pop' }], - [/.*/, 'string'] - ], - erDiagram: [ - [/[}|][o|](--|\.\.)[o|][{|]/, 'transition'], - [/".*?"/, 'string'], - [/(:)(.*?$)/, ['delimiter.bracket', 'string']], - [/:|{|}/, 'delimiter.bracket'], - [/([a-zA-Z]+)(\s+[a-zA-Z]+)/, ['type', 'variable']], - [/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'], - [/[a-zA-Z_-][\w$]*/, 'variable'] - ], - requirementDiagram: [ - [/->|<-|-/, 'transition'], - [/(\d+\.)*\d+/, 'number'], - [ - /[a-zA-Z_-][\w$]*/, - { - cases: { - '@requirementDiagramBlockKeywords': 'typeKeyword', - '@default': 'variable' - } - } - ], - [/:|{|}|\//, 'delimiter.bracket'], - [/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'], - [/".*?"/, 'string'] - ] - } - }); - - monacoEditor.editor.defineTheme('mermaid-dark', { - base: 'vs-dark', - inherit: true, - rules: [ - { token: 'typeKeyword', foreground: '9650c8', fontStyle: 'bold' }, - { token: 'transition', foreground: '008800', fontStyle: 'bold' } - ], - colors: {} - }); - - monacoEditor.editor.defineTheme('mermaid', { - base: 'vs', - inherit: true, - rules: [ - { token: 'typeKeyword', foreground: '9650c8', fontStyle: 'bold' }, - { token: 'keyword', foreground: '649696' }, - { token: 'custom-error', foreground: 'ff0000', fontStyle: 'bold' }, - { token: 'string', foreground: 'AA8500' }, - { token: 'transition', foreground: '008800', fontStyle: 'bold' }, - { token: 'delimiter.bracket', foreground: '000000', fontStyle: 'bold' }, - { token: 'annotation', foreground: '4b4b96' }, - { token: 'number', foreground: '4b4b96' }, - { token: 'comment', foreground: '888c89' }, - { token: 'variable', foreground: 'A22889' }, - { token: 'type', foreground: '2BDEA8' } - ], - colors: {} - }); - - // Register a completion item provider for the mermaid language - monacoEditor.languages.registerCompletionItemProvider('mermaid', { - provideCompletionItems: () => { - const suggestions = [ - { - label: 'loop', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: ['loop ${1:Loop text}', '\t$0', 'end'].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Sequence Diagram Loops' - }, - { - label: 'alt', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: ['alt ${1:Describing text}', '\t$0', 'else', '\t', 'end'].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Alternative Path' - }, - { - label: 'opt', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: ['opt ${1:Describing text}', '\t$0', 'end'].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Optional Path' - }, - { - label: 'par', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: [ - 'par ${1:[Action 1]}', - '\t$0', - 'and ${2:[Action 2]}', - '\t', - 'and ${3:[Action 3]}', - '\t', - 'end' - ].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Parallel Actions' - }, - { - label: 'rect', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: ['rect ${1:rgb(0, 255, 0)}', '\t$0', 'end'].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Background Color' - }, - { - label: 'subgraph', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: ['subgraph ${1:title}', '\t$0', 'end'].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Subgraph' - }, - { - label: 'class', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: ['class ${1:className} {', '\t$0', '}'].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Class' - }, - { - label: 'state', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: ['state ${1:stateName} {', '\t$0', '}'].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'State' - }, - { - label: 'note', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: ['note ${1:right of State1}', '\t$0', 'end note'].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'State' - }, - { - label: 'section', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: ['section ${1:Go to work}', '\t$0'].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'User-journey Section' - }, - { - label: 'element', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: ['element ${1:test_entity} {', '\t$0', '}'].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Requirement Diagram Element' - }, - { - label: 'options', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: ['options', '{', ' $0', '}', 'end'].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Git Graph Options' - }, - ...requirementDiagrams.map((requirementDiagramType) => ({ - label: requirementDiagramType, - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: [ - requirementDiagramType + ' ${1:test_req} {', - '\tid: 1', - '\ttext: the test text.', - '\trisk: high', - '\tverifyMethod: test', - '}' - ].join('\n'), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: requirementDiagramType - .split(/(?=[A-Z])/) - .map((part) => part[0].toUpperCase() + part.slice(1)) - .join(' ') - })), - ...[ - ...new Set( - Object.values(keywords) - .map((diagramKeywords) => - Object.entries(diagramKeywords) - .filter((keywordType) => keywordType[0] !== 'annotations') - .map((entry) => entry[1]) - ) - .flat(2) - ) - ].map((keyword) => ({ - label: keyword, - kind: monacoEditor.languages.CompletionItemKind.Keyword, - insertText: keyword - })) - ]; - return { suggestions: suggestions }; - } - }); - - monacoEditor.languages.setLanguageConfiguration('mermaid', { - autoClosingPairs: [ - { - open: '(', - close: ')' - }, - { - open: '{', - close: '}' - }, - { - open: '[', - close: ']' - } - ], - brackets: [ - ['(', ')'], - ['{', '}'], - ['[', ']'] - ] - }); -}; diff --git a/src/lib/util/env.ts b/src/lib/util/env.ts index f5e22f308..128761f2a 100644 --- a/src/lib/util/env.ts +++ b/src/lib/util/env.ts @@ -1,2 +1,4 @@ -export const rendererUrl = import.meta.env.MERMAID_RENDERER_URL ?? 'https://mermaid.ink'; -export const krokiRendererUrl = import.meta.env.MERMAID_KROKI_RENDERER_URL ?? 'https://kroki.io'; +export const rendererUrl: string = + (import.meta.env.MERMAID_RENDERER_URL as string) ?? 'https://mermaid.ink'; +export const krokiRendererUrl: string = + (import.meta.env.MERMAID_KROKI_RENDERER_URL as string) ?? 'https://kroki.io'; diff --git a/src/lib/util/serde.ts b/src/lib/util/serde.ts new file mode 100644 index 000000000..1f7540fc8 --- /dev/null +++ b/src/lib/util/serde.ts @@ -0,0 +1,61 @@ +import { deflate, inflate } from 'pako'; +import { toUint8Array, fromUint8Array, toBase64, fromBase64 } from 'js-base64'; +import type { State } from '$lib/types'; + +interface Serde { + serialize: (state: string) => string; + deserialize: (state: string) => string; +} + +const base64Serde: Serde = { + serialize: (state: string): string => { + return toBase64(state, true); + }, + deserialize: (state: string): string => { + return fromBase64(state); + } +}; + +export const pakoSerde: Serde = { + serialize: (state: string): string => { + const data = new TextEncoder().encode(state); + const compressed = deflate(data, { level: 9 }); + return fromUint8Array(compressed, true); + }, + deserialize: (state: string): string => { + const data = toUint8Array(state); + return inflate(data, { to: 'string' }); + } +}; + +const serdes: { [key: string]: Serde } = { + base64: base64Serde, + pako: pakoSerde +}; + +type SerdeType = keyof typeof serdes; + +export const serializeState = (state: State): string => { + const json = JSON.stringify(state); + const defaultSerde: SerdeType = 'pako'; + const serialized = serdes[defaultSerde].serialize(json); + return `${defaultSerde}:${serialized}`; +}; + +export const deserializeState = (state: string): State => { + let type: SerdeType, serialized: string; + if (state.includes(':')) { + let tempType: string; + [tempType, serialized] = state.split(':'); + if (tempType in serdes) { + type = tempType; + } else { + throw new Error(`Unknown serde type: ${tempType}`); + } + } else { + type = 'base64'; + serialized = state; + } + const json = serdes[type].deserialize(serialized); + return JSON.parse(json) as State; +}; diff --git a/src/lib/util/state.ts b/src/lib/util/state.ts index 7d3599082..73157a543 100644 --- a/src/lib/util/state.ts +++ b/src/lib/util/state.ts @@ -1,9 +1,9 @@ import { writable, get, derived } from 'svelte/store'; import type { Readable } from 'svelte/store'; -import { toBase64, fromBase64 } from 'js-base64'; import { persist, localStorage } from '@macfja/svelte-persistent-store'; import type { State } from '$lib/types'; import { saveStatistics } from './stats'; +import { serializeState, deserializeState } from './serde'; export const defaultState: State = { code: `graph TD @@ -37,17 +37,16 @@ const urlParseFailedState = `graph TD click D href "https://github.com/mermaid-js/mermaid-live-editor/issues/new?assignees=&labels=bug&template=bug_report.md&title=Broken%20link" "Raise issue"`; export const codeStore = persist(writable(defaultState), localStorage(), 'codeStore'); -export const base64State: Readable = derived([codeStore], ([code], set) => { - set(toBase64(JSON.stringify(code), true)); +export const serializedState: Readable = derived([codeStore], ([code], set) => { + set(serializeState(code)); }); export const loadState = (data: string): void => { let state: State; + console.log('Loading', data); try { - const stateStr = fromBase64(data); - console.log(`Trying to load state: ${stateStr}`); - state = JSON.parse(stateStr); - const mermaidConfig = + state = deserializeState(data); + const mermaidConfig: { [key: string]: string } = typeof state.mermaid === 'string' ? JSON.parse(state.mermaid) : state.mermaid; if ( mermaidConfig.securityLevel && @@ -116,7 +115,7 @@ export const toggleDarkTheme = (dark: boolean): void => { }; export const initURLSubscription = (): void => { - base64State.subscribe((state: string) => { + serializedState.subscribe((state: string) => { history.replaceState(undefined, undefined, `#${state}`); }); }; diff --git a/src/routes/edit.svelte b/src/routes/edit.svelte index 341a75e9a..f097364f4 100644 --- a/src/routes/edit.svelte +++ b/src/routes/edit.svelte @@ -1,12 +1,12 @@