diff --git a/.forgejo/workflows/build.yml b/.github/workflows/release.yml similarity index 60% rename from .forgejo/workflows/build.yml rename to .github/workflows/release.yml index 4bfcc57..49834e5 100644 --- a/.forgejo/workflows/build.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,7 @@ -name: Build on Push and create Release on Tag +name: Create Release on Tag Push on: push: - branches: - - main tags: - "v*" @@ -22,7 +20,7 @@ jobs: node-version: 20 registry-url: "https://registry.npmjs.org" - # Install pnpm + # Install pnpm - name: Install pnpm uses: pnpm/action-setup@v4 id: pnpm-install @@ -30,12 +28,6 @@ jobs: version: 8 run_install: false - # Validate Tag Matches JSON Versions - - name: Validate Tag Matches JSON Versions - if: github.ref_type == 'tag' - run: | - node scripts/validate_tag.cjs ${{ github.ref }} - # Get pnpm store directory - name: Get pnpm store directory id: pnpm-cache @@ -60,22 +52,11 @@ jobs: - name: Build for production run: pnpm build - # Move file - - name: Move file - run: mkdir built; mv package.zip built/package.zip - - # Upload artifacts - - name: Upload artifacts - uses: actions/upload-artifact@v3 + - name: Release + uses: ncipollo/release-action@v1 with: - path: built/package.zip - overwrite: true - - # Create Forgejo Release - - name: Create Forgejo Release - if: github.ref_type == 'tag' - uses: actions/forgejo-release@v1 - with: - direction: upload - release-dir: built - token: ${{ secrets.FORGE_TOKEN }} + allowUpdates: true + artifactErrorsFailBuild: true + artifacts: "package.zip" + token: ${{ secrets.GITHUB_TOKEN }} + prerelease: false diff --git a/README.md b/README.md index f58614c..73c10ff 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,29 @@ # SiYuan js-draw Plugin -This plugin allows you to embed js-draw whiteboards anywhere in your SiYuan documents. +This plugin allows you to embed js-draw whiteboards anywhere in your SiYuan documents. ## Usage instructions -- Install the plugin from the marketplace. You can find it by searching for `js-draw`. -- To add a new drawing to your document: - 1. Type `/Insert Drawing` in your document, and select the correct menu entry - 2. The whiteboard editor will open in a new tab. Draw as you like, then click the Save button and close the tab. -- To edit the image later: - 1. Right-click on the image (or click the three dots on mobile), select "Plugin" > "Edit with js-draw" in the menu - 2. The editor tab will open, edit your file as you like, then click the Save button and close the tab. +1. Install the plugin + - Grab a release from the [Releases page](https://git.massive.box/massivebox/siyuan-jsdraw-plugin/releases) + - Unzip it in the folder `./data/plugins`, relatively to your SiYuan workspace. + > The plugin is not yet available in the official marketplace. I will try to publish it there soon! +2. Insert a drawing in your documents by typing `/Insert Drawing` in your document, and selecting the correct menu entry +3. The whiteboard editor will open in a new tab. Draw as you like, then click the Save button. It will also add a + drawing block to your document. +4. Click the Gear icon > Refresh to refresh the drawing block, if it's still displaying the old drawing. +5. Click the drawing block to open the editor again. ## Planned features -Check out the [Projects](https://git.massive.box/massivebox/siyuan-jsdraw-plugin/projects) tab! +- [ ] Auto-reload drawing blocks on drawing change +- [ ] Rename whiteboards +- [ ] Improve internationalization framework +- [ ] Default background color and grid options +- [ ] Respecting user theme for the editor +- And more! ## Contributing -Contributions are always welcome! Right now, I'm working on the core functionality and fixing bugs. +Contributions are always welcome! Right now, I'm working on the core functionality and fixing bugs. After that is done, I will need help with the internationalization, as, unfortunately, I don't speak Chinese. Please [contact me](mailto:box@massive.box) if you'd like to help! diff --git a/package.json b/package.json index 76504eb..78f409d 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "siyuan-jsdraw-plugin", - "version": "0.4.0", + "name": "plugin-sample-vite-svelte", + "version": "0.3.6", "type": "module", - "description": "Include a whiteboard for freehand drawing anywhere in your documents.", - "repository": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", - "homepage": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", - "author": "massivebox", + "description": "This is a sample plugin based on vite and svelte for Siyuan (https://b3log.org/siyuan)", + "repository": "", + "homepage": "", + "author": "frostime", "license": "MIT", "scripts": { "dev": "cross-env NODE_ENV=development VITE_SOURCEMAP=inline vite build --watch", @@ -36,7 +36,6 @@ }, "dependencies": { "@js-draw/material-icons": "^1.29.0", - "js-draw": "^1.29.0", - "ts-serializable": "^4.2.0" + "js-draw": "^1.29.0" } } diff --git a/plugin.json b/plugin.json index 8acdb80..0b3987f 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "name": "siyuan-jsdraw-plugin", "author": "massivebox", "url": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", - "version": "0.4.0", + "version": "0.1.1", "minAppVersion": "3.0.12", "backends": [ "windows", @@ -31,7 +31,7 @@ }, "funding": { "custom": [ - "https://s.massive.box/jsdraw-plugin-donate" + "" ] }, "keywords": [ diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index fbeb088..b6d2382 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -1,44 +1,3 @@ { - "insertDrawing": "Insert Drawing", - "editDrawing": "Edit with js-draw", - "errNoFileID": "File ID missing - couldn't open file.", - "errSyncIDNotFound": "Couldn't find SyncID in document for drawing, make sure you're trying to edit a drawing that is included in at least a note.", - "errCreateUnknown": "Unknown error while creating editor, please try again.", - "errInvalidBackgroundColor": "Invalid background color! Please enter an HEX color, like #000000 (black) or #FFFFFF (white). The old background color will be used.", - "drawing": "Drawing", - "settings": { - "name": "js-draw Plugin Settings", - "suggestedColors":{ - "white": "White", - "black": "Black", - "transparent": "Transparent", - "custom": "Custom", - "darkBlue": "Dark Blue", - "darkGray": "Dark Gray" - }, - "grid": { - "title": "Enable grid by default", - "description": "Enable to automatically turn on the grid on new drawings." - }, - "backgroundDropdown":{ - "title": "Background color", - "description": "Default background color for new drawings." - }, - "background": { - "title": "Custom background", - "description": "Hexadecimal code of the custom background color for new drawings.<br /><b>This setting is only applied if \"Background Color\" is set to \"Custom\"!</b>" - }, - "dialogOnDesktop": { - "title": "Open editor as dialog on desktop", - "description": "Dialog mode provides a larger drawing area, but it's not as handy to use as tabs (default).<br />The editor will always open as a dialog on mobile." - }, - "analytics": { - "title": "Analytics", - "description": "Enable to send anonymous usage data to the developer. <a href='https://s.massive.box/jsdraw-plugin-privacy'>Privacy Policy</a>" - }, - "restorePosition": { - "title": "Remember editor position", - "description": "When enabled, the editor will remember the zoom factor and position, and it will restore them the next time you open the drawing." - } - } + "insertDrawing": "Insert Drawing" } \ No newline at end of file diff --git a/public/webapp/button.js b/public/webapp/button.js index 8be8fd4..610799d 100644 --- a/public/webapp/button.js +++ b/public/webapp/button.js @@ -1,15 +1,12 @@ -function copyEditLink(path) { - navigator.clipboard.writeText(getEditLink(path)); -} -function copyImageLink(path) { - navigator.clipboard.writeText(``); +function copyEditLink(fileID) { + navigator.clipboard.writeText(getEditLink(fileID)); } function refreshPage() { window.location.reload(); } -function addButton(document, path) { +function addButton(document, fileID) { // Add floating button const floatingButton = document.createElement('button'); @@ -22,8 +19,8 @@ function addButton(document, path) { popupMenu.id = 'popupMenu'; popupMenu.innerHTML = ` <button onclick="refreshPage()">Refresh</button> - <button onclick="copyEditLink('${path}')">Copy Direct Edit Link</button> - <button onclick="copyImageLink('${path}')">Copy Image Link</button> + <button onclick="copyEditLink('${fileID}')">Copy Direct Edit Link</button> + `; document.body.appendChild(popupMenu); @@ -34,7 +31,6 @@ function addButton(document, path) { document.body.addEventListener('mouseleave', () => { floatingButton.style.display = 'none'; - popupMenu.style.display = 'none'; }); // Toggle popup menu on button click diff --git a/public/webapp/cursor.png b/public/webapp/cursor.png deleted file mode 100644 index 1306cf3..0000000 Binary files a/public/webapp/cursor.png and /dev/null differ diff --git a/public/webapp/draw.js b/public/webapp/draw.js index 187dab2..9d9adfc 100644 --- a/public/webapp/draw.js +++ b/public/webapp/draw.js @@ -27,9 +27,9 @@ async function getFile(path) { } -async function getSVG(path) { +async function getSVG(fileID) { - const resp = await getFile("/data/" + path); + const resp = await getFile("/data/assets/" + fileID + '.svg'); if(resp == null) { return FALLBACK; } @@ -37,10 +37,10 @@ async function getSVG(path) { } -function getEditLink(path) { +function getEditLink(fileID) { const data = encodeURIComponent( JSON.stringify({ - path: path, + id: fileID }) ) return `siyuan://plugins/siyuan-jsdraw-pluginwhiteboard/?icon=iconDraw&title=Drawing&data=${data}`; diff --git a/public/webapp/error.html b/public/webapp/error.html deleted file mode 100644 index 2281841..0000000 --- a/public/webapp/error.html +++ /dev/null @@ -1,21 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <title>Error</title> - <style> - body { - background-color: white; - color: black; - } - </style> - </head> - <body> - <p>It looks like an error occurred. You shouldn't be able to see this page.</p> - <p>No data has been deleted. Please excuse us for the inconvenience.</p> - <p> - Try reloading SiYuan, and if the error persists, open an issue at - <code>https://git.massive.box/massivebox/siyuan-jsdraw-plugin/issues</code> - or contact the developer directly via e-mail at <code>box@massive.box</code> - </p> - </body> -</html> \ No newline at end of file diff --git a/public/webapp/index.html b/public/webapp/index.html index 9a02454..a1fd2f6 100644 --- a/public/webapp/index.html +++ b/public/webapp/index.html @@ -5,22 +5,18 @@ <script src="button.js"></script> <script> const urlParams = new URLSearchParams(window.location.search); - let path = urlParams.get('path'); - if(path === null) { - const fileID = urlParams.get('id'); // legacy support - path = "assets/" + fileID + ".svg"; - } + const fileID = urlParams.get('id'); document.addEventListener('DOMContentLoaded', async () => { const editLink = document.createElement('a'); - editLink.href = "./error.html"; + editLink.href = getEditLink(fileID); document.body.appendChild(editLink); const htmlContainer = document.createElement('div'); - htmlContainer.innerHTML = await getSVG(path); + htmlContainer.innerHTML = await getSVG(fileID); editLink.appendChild(htmlContainer); - addButton(document, path); + addButton(document, fileID); }); </script> <link rel="stylesheet" href="index.css"> diff --git a/scripts/validate_tag.cjs b/scripts/validate_tag.cjs deleted file mode 100644 index c842ffc..0000000 --- a/scripts/validate_tag.cjs +++ /dev/null @@ -1,24 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const [tagName] = process.argv.slice(2); // Get tag from CLI arguments -if (!tagName) { - console.error('Error: No tag name provided.'); - process.exit(1); -} - -const TAG_VERSION = tagName.replace('refs/tags/v', ''); - -try { - const packageJson = JSON.parse(fs.readFileSync(path.resolve('package.json'), 'utf8')); - const pluginJson = JSON.parse(fs.readFileSync(path.resolve('plugin.json'), 'utf8')); - - if (TAG_VERSION !== packageJson.version || TAG_VERSION !== pluginJson.version) { - console.error(`Error: Tag version (${TAG_VERSION}) does not match package.json (${packageJson.version}) or plugin.json (${pluginJson.version})`); - process.exit(1); - } - console.log('Tag version matches both JSON files.'); -} catch (err) { - console.error('Failed to read or parse JSON files:', err.message); - process.exit(1); -} diff --git a/src/analytics.ts b/src/analytics.ts deleted file mode 100644 index 13d37be..0000000 --- a/src/analytics.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {getBackend, getFrontend} from "siyuan"; -import {JSON_MIME} from "@/const"; -import packageJson from '../package.json' assert { type: 'json' }; - -export class Analytics { - - private readonly enabled: boolean; - - private static readonly ENDPOINT = 'https://stats.massive.box/api/send_noua'; - private static readonly WEBSITE_ID = '0a1ebbc1-d702-4f64-86ed-f62dcde9b522'; - - constructor(enabled: boolean) { - this.enabled = enabled; - } - - async sendEvent(name: string) { - - if(!this.enabled) return; - - const sendData = (name == 'load' || name == 'install') ? - { - 'appVersion': window.navigator.userAgent.split(' ')[0], - 'pluginVersion': packageJson.version, - 'frontend': getFrontend(), - 'backend': getBackend(), - 'language': navigator.language, - } : {}; - - await fetch(Analytics.ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': JSON_MIME, - }, - body: JSON.stringify({ - type: 'event', - payload: { - website: Analytics.WEBSITE_ID, - name: name, - data: sendData, - }, - }) - }) - - } - -} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index 7c4bfca..0000000 --- a/src/config.ts +++ /dev/null @@ -1,177 +0,0 @@ -import {PluginFile} from "@/file"; -import {CONFIG_FILENAME, JSON_MIME, STORAGE_PATH} from "@/const"; -import {Plugin, showMessage} from "siyuan"; -import {SettingUtils} from "@/libs/setting-utils"; -import {getFirstDefined} from "@/helper"; - -export interface Options { - dialogOnDesktop: boolean - analytics: boolean - editorOptions: EditorOptions -} -export interface EditorOptions { - restorePosition: boolean; - grid: boolean - background: string -} - -export class PluginConfig { - - private file: PluginFile; - - options: Options; - private firstRun: boolean; - - getFirstRun() { return this.firstRun } - - constructor() { - this.file = new PluginFile(STORAGE_PATH, CONFIG_FILENAME, JSON_MIME); - } - - async load() { - this.firstRun = false; - await this.file.loadFromSiYuanFS(); - const jsonObj = JSON.parse(this.file.getContent()); - if(jsonObj == null) { - this.firstRun = true; - } - // if more than one fallback, the intermediate ones are from a legacy config file version - this.options = { - dialogOnDesktop: getFirstDefined(jsonObj?.dialogOnDesktop, false), - analytics: getFirstDefined(jsonObj?.analytics, true), - editorOptions: { - restorePosition: getFirstDefined(jsonObj?.editorOptions?.restorePosition, jsonObj?.restorePosition, true), - grid: getFirstDefined(jsonObj?.editorOptions?.grid, jsonObj?.grid, true), - background: getFirstDefined(jsonObj?.editorOptions?.background, jsonObj?.background, "#00000000") - }, - }; - } - - async save() { - this.file.setContent(JSON.stringify(this.options)); - await this.file.save(); - } - - setConfig(config: Options) { - this.options = config; - } - - static validateColor(hex: string) { - hex = hex.replace('#', ''); - return typeof hex === 'string' - && (hex.length === 6 || hex.length === 8) - && !isNaN(Number('0x' + hex)) - } - -} - -export class PluginConfigViewer { - - config: PluginConfig; - settingUtils: SettingUtils; - plugin: Plugin; - private readonly backgroundDropdownOptions; - - constructor(config: PluginConfig, plugin: Plugin) { - this.config = config; - this.plugin = plugin; - this.backgroundDropdownOptions = { - '#00000000': plugin.i18n.settings.suggestedColors.transparent, - 'CUSTOM': plugin.i18n.settings.suggestedColors.custom, - '#ffffff': plugin.i18n.settings.suggestedColors.white, - '#1e2227': plugin.i18n.settings.suggestedColors.darkBlue, - '#1e1e1e': plugin.i18n.settings.suggestedColors.darkGray, - '#000000': plugin.i18n.settings.suggestedColors.black, - } - this.populateSettingMenu(); - } - - async configSaveCallback(data) { - - let color = data.backgroundDropdown === "CUSTOM" ? data.background : data.backgroundDropdown; - if(!PluginConfig.validateColor(color)) { - showMessage(this.plugin.i18n.errInvalidBackgroundColor, 0, 'error'); - data.background = this.config.options.editorOptions.background; - this.settingUtils.set('background', data.background); - } - - this.config.setConfig({ - dialogOnDesktop: data.dialogOnDesktop, - analytics: data.analytics, - editorOptions: { - grid: data.grid, - background: color, - restorePosition: data.restorePosition, - } - }); - await this.config.save(); - - } - - populateSettingMenu() { - - this.settingUtils = new SettingUtils({ - plugin: this.plugin, - name: 'optionsUI', - callback: async (data) => { - await this.configSaveCallback(data); - } - }); - - this.settingUtils.addItem({ - key: "grid", - title: this.plugin.i18n.settings.grid.title, - description: this.plugin.i18n.settings.grid.description, - value: this.config.options.editorOptions.grid, - type: 'checkbox' - }); - - this.settingUtils.addItem({ - key: 'backgroundDropdown', - title: this.plugin.i18n.settings.backgroundDropdown.title, - description: this.plugin.i18n.settings.backgroundDropdown.description, - type: 'select', - value: this.config.options.editorOptions.background in this.backgroundDropdownOptions ? - this.config.options.editorOptions.background : 'CUSTOM', - options: this.backgroundDropdownOptions, - }); - - this.settingUtils.addItem({ - key: "background", - title: this.plugin.i18n.settings.background.title, - description: this.plugin.i18n.settings.background.description, - value: this.config.options.editorOptions.background, - type: 'textinput', - }); - - this.settingUtils.addItem({ - key: "restorePosition", - title: this.plugin.i18n.settings.restorePosition.title, - description: this.plugin.i18n.settings.restorePosition.description, - value: this.config.options.editorOptions.restorePosition, - type: 'checkbox' - }); - - this.settingUtils.addItem({ - key: "dialogOnDesktop", - title: this.plugin.i18n.settings.dialogOnDesktop.title, - description: this.plugin.i18n.settings.dialogOnDesktop.description, - value: this.config.options.dialogOnDesktop, - type: 'checkbox' - }); - - this.settingUtils.addItem({ - key: "analytics", - title: this.plugin.i18n.settings.analytics.title, - description: this.plugin.i18n.settings.analytics.description, - value: this.config.options.analytics, - type: 'checkbox' - }); - - } - - load() { - return this.settingUtils.load(); - } - -} \ No newline at end of file diff --git a/src/const.ts b/src/const.ts index b7d60aa..1acaa80 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,9 +1,7 @@ export const SVG_MIME = "image/svg+xml"; export const JSON_MIME = "application/json"; -export const DATA_PATH = "/data/"; -export const ASSETS_PATH = "assets/"; -export const STORAGE_PATH = "/data/storage/petal/siyuan-jsdraw-plugin/"; -export const TOOLBAR_FILENAME = "toolbar.json"; -export const CONFIG_FILENAME = "conf.json"; -export const EMBED_PATH = "/plugins/siyuan-jsdraw-plugin/webapp/?path="; -export const DUMMY_HOST = "https://dummy.host/"; \ No newline at end of file +export const DATA_PATH = "/data/assets"; +export const STORAGE_PATH = "/data/storage/petal/siyuan-jsdraw-plugin"; +export const TOOLBAR_PATH = STORAGE_PATH + "/toolbar.json"; +export const CONFIG_PATH = STORAGE_PATH + "/conf.json"; +export const EMBED_PATH = "/plugins/siyuan-jsdraw-plugin/webapp/?id="; \ No newline at end of file diff --git a/src/editor.ts b/src/editor.ts deleted file mode 100644 index 1331e88..0000000 --- a/src/editor.ts +++ /dev/null @@ -1,251 +0,0 @@ -import {MaterialIconProvider} from "@js-draw/material-icons"; -import {PluginAsset, PluginFile} from "@/file"; -import {JSON_MIME, STORAGE_PATH, SVG_MIME, TOOLBAR_FILENAME} from "@/const"; -import Editor, { - BackgroundComponentBackgroundType, - BaseWidget, - Color4, - EditorEventType, - Mat33, - Vec2, - Viewport -} from "js-draw"; -import {Dialog, getFrontend, openTab, Plugin, showMessage} from "siyuan"; -import {findSyncIDInProtyle, replaceSyncID} from "@/protyle"; -import DrawJSPlugin from "@/index"; -import {EditorOptions} from "@/config"; -import 'js-draw/styles'; -import {SyncIDNotFoundError, UnchangedProtyleError} from "@/errors"; - -export class PluginEditor { - - private readonly element: HTMLElement; - private readonly editor: Editor; - - private drawingFile: PluginAsset; - private toolbarFile: PluginFile; - - private readonly fileID: string; - private syncID: string; - - getElement(): HTMLElement { return this.element; } - getEditor(): Editor { return this.editor; } - getFileID(): string { return this.fileID; } - getSyncID(): string { return this.syncID; } - setSyncID(syncID: string) { this.syncID = syncID; } - - private constructor(fileID: string) { - - this.fileID = fileID; - - this.element = document.createElement("div"); - this.element.style.height = '100%'; - this.editor = new Editor(this.element, { - iconProvider: new MaterialIconProvider(), - }); - - const styleElement = document.createElement('style'); - styleElement.innerHTML = ` - canvas.wetInkCanvas { - cursor: url('/plugins/siyuan-jsdraw-plugin/webapp/cursor.png') 6 6, auto; - } - `; - this.element.appendChild(styleElement); - - this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false); - this.editor.getRootElement().style.height = '100%'; - - } - - static async create(fileID: string, defaultEditorOptions: EditorOptions): Promise<PluginEditor> { - - const instance = new PluginEditor(fileID); - - await instance.genToolbar(); - let syncID = await findSyncIDInProtyle(fileID); - - if(syncID == null) { - throw new SyncIDNotFoundError(fileID); - } - instance.setSyncID(syncID); - await instance.restoreOrInitFile(defaultEditorOptions); - - return instance; - - } - - async restoreOrInitFile(defaultEditorOptions: EditorOptions) { - - this.drawingFile = new PluginAsset(this.fileID, this.syncID, SVG_MIME); - await this.drawingFile.loadFromSiYuanFS(); - const drawingContent = this.drawingFile.getContent(); - - if(drawingContent != null) { - - await this.editor.loadFromSVG(drawingContent); - - // restore position and zoom - const svgElem = new DOMParser().parseFromString(drawingContent, SVG_MIME).documentElement; - const editorViewStr = svgElem.getAttribute('editorView'); - if(editorViewStr != null && defaultEditorOptions.restorePosition) { - try { - const [viewBoxOriginX, viewBoxOriginY, zoom] = editorViewStr.split(' ').map(x => parseFloat(x)); - this.editor.dispatch(Viewport.transformBy(Mat33.scaling2D(zoom))); - this.editor.dispatch(Viewport.transformBy(Mat33.translation(Vec2.of( - - viewBoxOriginX, - - viewBoxOriginY - )))); - }catch (e){} - } - - }else{ - // it's a new drawing - this.editor.dispatch(this.editor.setBackgroundStyle({ - color: Color4.fromHex(defaultEditorOptions.background), - type: defaultEditorOptions.grid ? BackgroundComponentBackgroundType.Grid : BackgroundComponentBackgroundType.SolidColor, - autoresize: true - })); - } - - } - - async genToolbar() { - - const toolbar = this.editor.addToolbar(); - - // save button - const saveButton = toolbar.addSaveButton(async () => { - await this.saveCallback(saveButton); - }); - - // restore toolbarFile state - this.toolbarFile = new PluginFile(STORAGE_PATH, TOOLBAR_FILENAME, JSON_MIME); - await this.toolbarFile.loadFromSiYuanFS(); - if(this.toolbarFile.getContent() != null) { - toolbar.deserializeState(this.toolbarFile.getContent()); - } - - // save toolbar config on tool change (toolbar state is not saved in SVGs!) - this.editor.notifier.on(EditorEventType.ToolUpdated, () => { - this.toolbarFile.setContent(toolbar.serializeState()); - this.toolbarFile.save(); - }); - - } - - private async saveCallback(saveButton: BaseWidget) { - - const svgElem = this.editor.toSVG(); - let newSyncID: string; - const oldSyncID = this.syncID; - - const rect = this.editor.viewport.visibleRect; - const zoom = this.editor.viewport.getScaleFactor(); - svgElem.setAttribute('editorView', `${rect.x} ${rect.y} ${zoom}`) - - try { - this.drawingFile.setContent(svgElem.outerHTML); - await this.drawingFile.save(); - newSyncID = this.drawingFile.getSyncID(); - if(newSyncID != oldSyncID) { // supposed to replace protyle - const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID); // try to change protyle - if(!changed) throw new UnchangedProtyleError(); - await this.drawingFile.removeOld(oldSyncID); - } - saveButton.setDisabled(true); - setTimeout(() => { // @todo improve save button feedback - saveButton.setDisabled(false); - }, 500); - } catch (error) { - showMessage("Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there.", 0, 'error'); - if(error instanceof UnchangedProtyleError) { - showMessage("Make sure the image you're trying to edit still exists in your documents.", 0, 'error'); - } - await navigator.clipboard.writeText(svgElem.outerHTML); - console.error(error); - console.log("Couldn't save SVG: ", svgElem.outerHTML) - return; - } - - this.syncID = newSyncID; - - } - -} - -export class EditorManager { - - private editor: PluginEditor - setEditor(editor: PluginEditor) { this.editor = editor;} - - static async create(fileID: string, p: DrawJSPlugin) { - let instance = new EditorManager(); - try { - let editor = await PluginEditor.create(fileID, p.config.options.editorOptions); - instance.setEditor(editor); - }catch (error) { - EditorManager.handleCreationError(error, p); - } - return instance; - } - - static registerTab(p: DrawJSPlugin) { - p.addTab({ - 'type': "whiteboard", - async init() { - const fileID = this.data.fileID; - if (fileID == null) { - alert(p.i18n.errNoFileID); - return; - } - try { - const editor = await PluginEditor.create(fileID, p.config.options.editorOptions); - this.element.appendChild(editor.getElement()); - }catch (error){ - EditorManager.handleCreationError(error, p); - } - } - }); - } - - static handleCreationError(error: any, p: DrawJSPlugin) { - console.error(error); - let errorTxt = p.i18n.errCreateUnknown; - if(error instanceof SyncIDNotFoundError) { - errorTxt = p.i18n.errSyncIDNotFound; - } - showMessage(errorTxt, 0, 'error'); - } - - toTab(p: Plugin) { - openTab({ - app: p.app, - custom: { - title: p.i18n.drawing, - icon: 'iconDraw', - id: "siyuan-jsdraw-pluginwhiteboard", - data: { - fileID: this.editor.getFileID(), - } - } - }); - } - - toDialog() { - const dialog = new Dialog({ - width: "100vw", - height: getFrontend() == "mobile" ? "100vh" : "90vh", - content: `<div id="DrawingPanel" style="width:100%; height: 100%;"></div>`, - }); - dialog.element.querySelector("#DrawingPanel").appendChild(this.editor.getElement()); - } - - open(p: DrawJSPlugin) { - if(getFrontend() != "mobile" && !p.config.options.dialogOnDesktop) { - this.toTab(p); - } else { - this.toDialog(); - } - } - -} \ No newline at end of file diff --git a/src/editorTab.ts b/src/editorTab.ts new file mode 100644 index 0000000..bef75c7 --- /dev/null +++ b/src/editorTab.ts @@ -0,0 +1,75 @@ +import {ITabModel, openTab, Plugin} from "siyuan" +import Editor, {BaseWidget, EditorEventType} from "js-draw"; +import { MaterialIconProvider } from '@js-draw/material-icons'; +import 'js-draw/styles'; +import {getFile, saveFile} from "@/file"; +import {JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const"; +import {idToPath} from "@/helper"; + +export function openEditorTab(p: Plugin, fileID: string) { + openTab({ + app: p.app, + custom: { + title: 'Drawing', + icon: 'iconDraw', + id: "siyuan-jsdraw-pluginwhiteboard", + data: { id: fileID } + } + }); +} + +async function saveCallback(editor: Editor, fileID: string, saveButton: BaseWidget) { + const svgElem = editor.toSVG(); + try { + saveFile(idToPath(fileID), SVG_MIME, svgElem.outerHTML); + saveButton.setDisabled(true); + setTimeout(() => { // @todo improve save button feedback + saveButton.setDisabled(false); + }, 500); + } catch (error) { + alert("Error saving drawing! Enter developer mode to find the error, and a copy of the current status."); + console.error(error); + console.log("Couldn't save SVG: ", svgElem.outerHTML) + } + +} + +export function createEditor(i: ITabModel) { + + const fileID = i.data.id; + if(fileID == null) { + alert("File ID missing - couldn't open file.") + return; + } + + const editor = new Editor(i.element, { + iconProvider: new MaterialIconProvider(), + }); + + const toolbar = editor.addToolbar(); + + // restore toolbar state + getFile(TOOLBAR_PATH).then(toolbarState => { + if(toolbarState!= null) { + toolbar.deserializeState(toolbarState) + } + }); + // restore drawing + getFile(idToPath(fileID)).then(svg => { + if(svg != null) { + editor.loadFromSVG(svg); + } + }); + + // save logic + const saveButton = toolbar.addSaveButton(() => saveCallback(editor, fileID, saveButton)); + + // save toolbar config on tool change (toolbar state is not saved in SVGs!) + editor.notifier.on(EditorEventType.ToolUpdated, () => { + saveFile(TOOLBAR_PATH, JSON_MIME, toolbar.serializeState()); + }); + + editor.dispatch(editor.setBackgroundStyle({ autoresize: true }), false); + editor.getRootElement().style.height = '100%'; + +} \ No newline at end of file diff --git a/src/errors.ts b/src/errors.ts deleted file mode 100644 index 914bd9c..0000000 --- a/src/errors.ts +++ /dev/null @@ -1,12 +0,0 @@ - -export class SyncIDNotFoundError extends Error { - readonly fileID: string; - - constructor(fileID: string) { - super(`SyncID not found for file ${fileID}`); - this.fileID = fileID; - Object.setPrototypeOf(this, new.target.prototype); - } -} - -export class UnchangedProtyleError extends Error {} \ No newline at end of file diff --git a/src/file.ts b/src/file.ts index dc2a86c..5ce0d20 100644 --- a/src/file.ts +++ b/src/file.ts @@ -1,108 +1,37 @@ -import {getFileBlob, putFile, removeFile, upload} from "@/api"; -import {ASSETS_PATH, DATA_PATH} from "@/const"; -import {assetPathToIDs, IDsToAssetName} from "@/helper"; +import {getFileBlob, putFile} from "@/api"; -abstract class PluginFileBase { +function toFile(title: string, content: string, mimeType: string){ + const blob = new Blob([content], { type: mimeType }); + return new File([blob], title, { type: mimeType }); +} - protected content: string | null; +export function saveFile(path: string, mimeType: string, content: string) { - protected fileName: string; - protected folderPath: string; - protected mimeType: string; + const file = toFile(path.split('/').pop(), content, mimeType); - getContent() { return this.content; } - setContent(content: string) { this.content = content; } - setFileName(fileName: string) { this.fileName = fileName; } - - private setFolderPath(folderPath: string) { - if(folderPath.startsWith('/') && folderPath.endsWith('/')) { - this.folderPath = folderPath; - }else{ - throw new Error("folderPath must start and end with /"); - } - } - - // folderPath must start and end with / - constructor(folderPath: string, fileName: string, mimeType: string) { - this.setFolderPath(folderPath); - this.fileName = fileName; - this.mimeType = mimeType; - } - - async loadFromSiYuanFS() { - const blob = await getFileBlob(this.folderPath + this.fileName); - const text = await blob.text(); - - try { - const res = JSON.parse(text); - if(res.code == 404) { - this.content = null; - return; - } - }catch {} - - this.content = text; - } - - async remove(customFilename?: string) { - let filename = customFilename || this.fileName; - await removeFile(this.folderPath + filename); - } - - protected toFile(customFilename?: string): File { - let filename = customFilename || this.fileName; - const blob = new Blob([this.content], { type: this.mimeType }); - return new File([blob], filename, { type: this.mimeType }); + try { + putFile(path, false, file); + } catch (error) { + console.error("Error saving file:", error); + throw error; } } -export class PluginFile extends PluginFileBase { +export async function getFile(path: string) { - async save() { - const file = this.toFile(); - try { - await putFile(this.folderPath + this.fileName, false, file); - } catch (error) { - console.error("Error saving file:", error); - throw error; + const blob = await getFileBlob(path); + const jsonText = await blob.text(); + + // if we got a 404 api response, we will return null + try { + const res = JSON.parse(jsonText); + if(res.code == 404) { + return null; } - } + }catch {} + + // js-draw expects a string! + return jsonText; } - -export class PluginAsset extends PluginFileBase { - - private fileID: string - private syncID: string - - getFileID() { return this.fileID; } - getSyncID() { return this.syncID; } - - constructor(fileID: string, syncID: string, mimeType: string) { - super(DATA_PATH + ASSETS_PATH, IDsToAssetName(fileID, syncID), mimeType); - this.fileID = fileID; - this.syncID = syncID; - } - - async save() { - - const file = this.toFile(this.fileID + '.svg'); - - let r = await upload('/' + ASSETS_PATH, [file]); - if (r.errFiles) { - throw new Error("Failed to upload file"); - } - const ids = assetPathToIDs(r.succMap[file.name]) - - this.fileID = ids.fileID; - this.syncID = ids.syncID; - super.setFileName(IDsToAssetName(this.fileID, this.syncID)); - - } - - async removeOld(oldSyncID: string) { - await super.remove(IDsToAssetName(this.fileID, oldSyncID)); - } - -} \ No newline at end of file diff --git a/src/helper.ts b/src/helper.ts index 7041ba5..18386c7 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,5 +1,5 @@ import { Plugin } from 'siyuan'; -import {ASSETS_PATH} from "@/const"; +import {DATA_PATH, EMBED_PATH} from "@/const"; const drawIcon: string = ` <symbol id="iconDraw" viewBox="0 0 28 28"> @@ -23,7 +23,7 @@ export function getMenuHTML(icon: string, text: string): string { `; } -export function generateTimeString() { +export function generateSiyuanId() { const now = new Date(); const year = now.getFullYear().toString(); @@ -33,84 +33,25 @@ export function generateTimeString() { const minutes = now.getMinutes().toString().padStart(2, '0'); const seconds = now.getSeconds().toString().padStart(2, '0'); - return `${year}${month}${day}${hours}${minutes}${seconds}`; -} - -export function generateRandomString() { + const timestamp = `${year}${month}${day}${hours}${minutes}${seconds}`; const characters = 'abcdefghijklmnopqrstuvwxyz'; let random = ''; for (let i = 0; i < 7; i++) { random += characters.charAt(Math.floor(Math.random() * characters.length)); } - return random; + return `${timestamp}-${random}`; } -export function IDsToAssetName(fileID: string, syncID: string) { - return `${fileID}-${syncID}.svg`; -} -export function IDsToAssetPath(fileID: string, syncID: string) { - return `${ASSETS_PATH}${IDsToAssetName(fileID, syncID)}` -} -export function assetPathToIDs(assetPath: string): { fileID: string; syncID: string } | null { - - const filename = assetPath.split('/').pop() || ''; - if (!filename.endsWith('.svg')) return null; - - // Split into [basename, extension] and check format - const [basename] = filename.split('.'); - const parts = basename.split('-'); - - // Must contain exactly 2 hyphens separating 3 non-empty parts - if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) return null; - - return { - fileID: parts[0], - syncID: parts[1] + '-' + parts[2] - }; - +export function idToPath(id: string) { + return DATA_PATH + '/' + id + '.svg'; } -export function getMarkdownBlock(fileID: string, syncID: string): string { +// [Edit](siyuan://plugins/siyuan-jsdraw-pluginwhiteboard/?icon=iconDraw&title=Drawing&data={"id":"${id}"}) +//  +export function getPreviewHTML(id: string): string { return ` - }) + <iframe src="${EMBED_PATH + id}&antiCache=0"></iframe> ` -} - -// given a tag (such as a div) containing an image as a child at any level, return the src of the image -export function findImgSrc(element: HTMLElement): string | null { - // Base case: if current element is an image - if (element.tagName === 'IMG') { - return (element as HTMLImageElement).src; - } - - // Recursively check children - if (element.children) { - for (const child of Array.from(element.children)) { - const src = findImgSrc(child as HTMLElement); - if (src) return src; - } - } - - return null; -} - -export function imgSrcToIDs(imgSrc: string | null): { fileID: string; syncID: string } | null { - - if (!imgSrc) return null; - - const url = new URL(imgSrc); - imgSrc = decodeURIComponent(url.pathname); - - return assetPathToIDs(imgSrc); - -} - -export function getFirstDefined(...a) { - for(let i = 0; i < a.length; i++) { - if(a[i] !== undefined) { - return a[i]; - } - } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9d02d6d..e19ecb2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,82 +1,44 @@ import {Plugin, Protyle} from 'siyuan'; -import { - getMarkdownBlock, - loadIcons, - getMenuHTML, - findImgSrc, - imgSrcToIDs, generateTimeString, generateRandomString -} from "@/helper"; -import {migrate} from "@/migration"; -import {EditorManager} from "@/editor"; -import {PluginConfig, PluginConfigViewer} from "@/config"; -import {Analytics} from "@/analytics"; +import {getPreviewHTML, loadIcons, getMenuHTML, generateSiyuanId} from "@/helper"; +import {createEditor, openEditorTab} from "@/editorTab"; export default class DrawJSPlugin extends Plugin { - - config: PluginConfig; - analytics: Analytics; - - async onload() { + onload() { loadIcons(this); - EditorManager.registerTab(this); - migrate() - - await this.startConfig(); - await this.startAnalytics(); + //const id = Math.random().toString(36).substring(7); + this.addTab({ + 'type': "whiteboard", + init() { + createEditor(this); + } + }); this.protyleSlash = [{ id: "insert-drawing", filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"], html: getMenuHTML("iconDraw", this.i18n.insertDrawing), - callback: async (protyle: Protyle) => { - void this.analytics.sendEvent('create'); - const fileID = generateRandomString(); - const syncID = generateTimeString() + '-' + generateRandomString(); - protyle.insert(getMarkdownBlock(fileID, syncID), true, false); - (await EditorManager.create(fileID, this)).open(this); + callback: (protyle: Protyle) => { + const uid = generateSiyuanId(); + protyle.insert(getPreviewHTML(uid), true, false); + openEditorTab(this, uid); } }]; - this.eventBus.on("open-menu-image", (e: any) => { - const ids = imgSrcToIDs(findImgSrc(e.detail.element)); - if (ids === null) return; - e.detail.menu.addItem({ - icon: "iconDraw", - label: this.i18n.editDrawing, - click: async () => { - void this.analytics.sendEvent('edit'); - (await EditorManager.create(ids.fileID, this)).open(this); - } - }) - }) + } + onLayoutReady() { + // This function is automatically called when the layout is loaded. } onunload() { - void this.analytics.sendEvent("unload"); + // This function is automatically called when the plugin is disabled. } uninstall() { - void this.analytics.sendEvent("uninstall"); + // This function is automatically called when the plugin is uninstalled. } - private async startConfig() { - this.config = new PluginConfig(); - await this.config.load(); - let configViewer = new PluginConfigViewer(this.config, this); - await configViewer.load(); - } - - private async startAnalytics() { - this.analytics = new Analytics(this.config.options.analytics); - if(this.config.getFirstRun()) { - await this.config.save(); - void this.analytics.sendEvent('install'); - }else{ - void this.analytics.sendEvent('load'); - } - } } \ No newline at end of file diff --git a/src/migration.ts b/src/migration.ts deleted file mode 100644 index 2829aad..0000000 --- a/src/migration.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {sql} from "@/api"; -import {PluginAsset, PluginFile} from "@/file"; -import {ASSETS_PATH, DATA_PATH, SVG_MIME} from "@/const"; -import {replaceBlockContent} from "@/protyle"; -import {generateRandomString, getMarkdownBlock} from "@/helper"; -import {Dialog} from "siyuan"; - -export async function migrate() { - - let blocks = await findEmbedBlocks(); - const found = blocks.length > 0; - - for(const block of blocks) { - const oldFileID = extractID(block.markdown); - if(oldFileID) { - const oldFile = new PluginFile(DATA_PATH + ASSETS_PATH, oldFileID + '.svg', SVG_MIME); - await oldFile.loadFromSiYuanFS(); - const newFile = new PluginAsset(generateRandomString(), oldFileID, SVG_MIME); - newFile.setContent(oldFile.getContent()); - await newFile.save(); - const newMarkdown = getMarkdownBlock(newFile.getFileID(), newFile.getSyncID()); - if(await replaceBlockContent(block.id, block.markdown, newMarkdown)) { - await oldFile.remove(); - } - } - } - - if(found) { - new Dialog({ - width: "90vw", - height: "90vh", - content: ` - <iframe - style="width: 100%; height: 100%; background-color: white" - src="https://notes.massive.box/YRpTbbxLiD" - /> - ` - }) - } - -} - -function extractID(html: string): string | null { - // Match the pattern: id= followed by characters until & or quote - const regex = /id=([^&"']+)/; - const match = html.match(regex); - return match ? match[1] : null; -} - -async function findEmbedBlocks() { - - const sqlQuery = ` - SELECT id, markdown - FROM blocks - WHERE markdown like '%src="/plugins/siyuan-jsdraw-plugin/webapp/%' - `; - - try { - return await sql(sqlQuery); - } catch (error) { - console.error('Error searching for embed blocks:', error); - return []; - } - -} \ No newline at end of file diff --git a/src/protyle.ts b/src/protyle.ts deleted file mode 100644 index 5c3e842..0000000 --- a/src/protyle.ts +++ /dev/null @@ -1,112 +0,0 @@ -import {getBlockByID, sql, updateBlock} from "@/api"; -import {assetPathToIDs, IDsToAssetPath} from "@/helper"; - -export async function findSyncIDInProtyle(fileID: string, iter?: number): Promise<string> { - - const search = `assets/${fileID}-`; - const blocks = await findImageBlocks(search); - - let syncID = null; - - for(const block of blocks) { - const sources = extractImageSourcesFromMarkdown(block.markdown, search); - for(const source of sources) { - const ids = assetPathToIDs(source); - if(syncID == null) { - syncID = ids.syncID; - }else if(ids.syncID !== syncID) { - throw new Error( - "Multiple syncIDs found in documents. Remove the drawings that don't exist from your documents.\n" + - "Sync conflict copies can cause this error, so make sure to delete them, or at least the js-draw drawings they contain.\n" + - "File IDs must be unique. Close this editor tab now." - ); - } - } - } - - if(!iter) iter = 0; - if(syncID == null) { - // when the block has just been created, we need to wait a bit before it can be found - if(iter < 4) { // cap max time at 2s, it should be ok by then - await new Promise(resolve => setTimeout(resolve, 500)); - return await findSyncIDInProtyle(fileID, iter + 1); - } - } - - return syncID; - -} - -export async function findImageBlocks(src: string) { - - const sqlQuery = ` - SELECT id, markdown - FROM blocks - WHERE markdown like '%](${src}%' // "](" is to check it's an image src - `; - - try { - return await sql(sqlQuery); - } catch (error) { - console.error('Error searching for image blocks:', error); - return []; - } - -} -export async function replaceBlockContent( - blockId: string, - searchStr: string, - replaceStr: string -): Promise<boolean> { - try { - - const block = await getBlockByID(blockId); - if (!block) { - throw new Error('Block not found'); - } - - const originalContent = block.markdown; - const newContent = originalContent.replaceAll(searchStr, replaceStr); - - if (newContent === originalContent) { - return false; - } - - await updateBlock('markdown', newContent, blockId); - return true; - - } catch (error) { - console.error('Failed to replace block content:', error); - return false; - } -} - -function extractImageSourcesFromMarkdown(markdown: string, mustStartWith?: string) { - const imageRegex = /!\[.*?\]\((.*?)\)/g; // only get images - return Array.from(markdown.matchAll(imageRegex)) - .map(match => match[1]) - .filter(source => source.startsWith(mustStartWith)) // discard other images -} - -export async function replaceSyncID(fileID: string, oldSyncID: string, newSyncID: string) { - - const search = encodeURI(IDsToAssetPath(fileID, oldSyncID)); // the API uses URI-encoded - // find blocks containing that image - const blocks = await findImageBlocks(search); - if(blocks.length === 0) return false; - - for(const block of blocks) { - - // get all the image sources, with parameters - const markdown = block.markdown; - - for(const source of extractImageSourcesFromMarkdown(markdown, search)) { - const newSource = IDsToAssetPath(fileID, newSyncID); - const changed = await replaceBlockContent(block.id, source, newSource); - if(!changed) return false - } - - } - return true; - -} diff --git a/tsconfig.json b/tsconfig.json index e2dedbd..0fcc1ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "useDefineForClassFields": true, "module": "ESNext", "lib": [ - "ES2021", + "ES2020", "DOM", "DOM.Iterable" ], diff --git a/vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs b/vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs deleted file mode 100644 index f2c6618..0000000 --- a/vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs +++ /dev/null @@ -1,185 +0,0 @@ -// vite.config.ts -import { resolve as resolve2 } from "path"; -import { defineConfig } from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/vite/dist/node/index.js"; -import { viteStaticCopy } from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/vite-plugin-static-copy/dist/index.js"; -import livereload from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/rollup-plugin-livereload/dist/index.cjs.js"; -import { svelte } from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; -import zipPack from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/vite-plugin-zip-pack/dist/esm/index.mjs"; -import fg from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/fast-glob/out/index.js"; - -// yaml-plugin.js -import fs from "fs"; -import yaml from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/js-yaml/dist/js-yaml.mjs"; -import { resolve } from "path"; -function vitePluginYamlI18n(options = {}) { - const DefaultOptions = { - inDir: "src/i18n", - outDir: "dist/i18n" - }; - const finalOptions = { ...DefaultOptions, ...options }; - return { - name: "vite-plugin-yaml-i18n", - buildStart() { - console.log("\u{1F308} Parse I18n: YAML to JSON.."); - const inDir = finalOptions.inDir; - const outDir = finalOptions.outDir; - if (!fs.existsSync(outDir)) { - fs.mkdirSync(outDir, { recursive: true }); - } - const files = fs.readdirSync(inDir); - for (const file of files) { - if (file.endsWith(".yaml") || file.endsWith(".yml")) { - console.log(`-- Parsing ${file}`); - const jsonFile = file.replace(/\.(yaml|yml)$/, ".json"); - if (files.includes(jsonFile)) { - console.log(`---- File ${jsonFile} already exists, skipping...`); - continue; - } - try { - const filePath = resolve(inDir, file); - const fileContents = fs.readFileSync(filePath, "utf8"); - const parsed = yaml.load(fileContents); - const jsonContent = JSON.stringify(parsed, null, 2); - const outputFilePath = resolve(outDir, file.replace(/\.(yaml|yml)$/, ".json")); - console.log(`---- Writing to ${outputFilePath}`); - fs.writeFileSync(outputFilePath, jsonContent); - } catch (error) { - this.error(`---- Error parsing YAML file ${file}: ${error.message}`); - } - } - } - } - }; -} - -// vite.config.ts -var __vite_injected_original_dirname = "/home/massive/Dev/siyuan-jsdraw-plugin"; -var env = process.env; -var isSrcmap = env.VITE_SOURCEMAP === "inline"; -var isDev = env.NODE_ENV === "development"; -var outputDir = isDev ? "dev" : "dist"; -console.log("isDev=>", isDev); -console.log("isSrcmap=>", isSrcmap); -console.log("outputDir=>", outputDir); -var vite_config_default = defineConfig({ - resolve: { - alias: { - "@": resolve2(__vite_injected_original_dirname, "src") - } - }, - plugins: [ - svelte(), - vitePluginYamlI18n({ - inDir: "public/i18n", - outDir: `${outputDir}/i18n` - }), - viteStaticCopy({ - targets: [ - { src: "./README*.md", dest: "./" }, - { src: "./plugin.json", dest: "./" }, - { src: "./preview.png", dest: "./" }, - { src: "./icon.png", dest: "./" } - ] - }) - ], - define: { - "process.env.DEV_MODE": JSON.stringify(isDev), - "process.env.NODE_ENV": JSON.stringify(env.NODE_ENV) - }, - build: { - outDir: outputDir, - emptyOutDir: false, - minify: true, - sourcemap: isSrcmap ? "inline" : false, - lib: { - entry: resolve2(__vite_injected_original_dirname, "src/index.ts"), - fileName: "index", - formats: ["cjs"] - }, - rollupOptions: { - plugins: [ - ...isDev ? [ - livereload(outputDir), - { - name: "watch-external", - async buildStart() { - const files = await fg([ - "public/i18n/**", - "./README*.md", - "./plugin.json" - ]); - for (let file of files) { - this.addWatchFile(file); - } - } - } - ] : [ - // Clean up unnecessary files under dist dir - cleanupDistFiles({ - patterns: ["i18n/*.yaml", "i18n/*.md"], - distDir: outputDir - }), - zipPack({ - inDir: "./dist", - outDir: "./", - outFileName: "package.zip" - }) - ] - ], - external: ["siyuan", "process"], - output: { - entryFileNames: "[name].js", - assetFileNames: (assetInfo) => { - if (assetInfo.name === "style.css") { - return "index.css"; - } - return assetInfo.name; - } - } - } - } -}); -function cleanupDistFiles(options) { - const { - patterns, - distDir - } = options; - return { - name: "rollup-plugin-cleanup", - enforce: "post", - writeBundle: { - sequential: true, - order: "post", - async handler() { - const fg2 = await import("file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/fast-glob/out/index.js"); - const fs2 = await import("fs"); - const distPatterns = patterns.map((pat) => `${distDir}/${pat}`); - console.debug("Cleanup searching patterns:", distPatterns); - const files = await fg2.default(distPatterns, { - dot: true, - absolute: true, - onlyFiles: false - }); - for (const file of files) { - try { - if (fs2.default.existsSync(file)) { - const stat = fs2.default.statSync(file); - if (stat.isDirectory()) { - fs2.default.rmSync(file, { recursive: true }); - } else { - fs2.default.unlinkSync(file); - } - console.log(`Cleaned up: ${file}`); - } - } catch (error) { - console.error(`Failed to clean up ${file}:`, error); - } - } - } - } - }; -} -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiLCAieWFtbC1wbHVnaW4uanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS9tYXNzaXZlL0Rldi9zaXl1YW4tanNkcmF3LXBsdWdpblwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL2hvbWUvbWFzc2l2ZS9EZXYvc2l5dWFuLWpzZHJhdy1wbHVnaW4vdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2hvbWUvbWFzc2l2ZS9EZXYvc2l5dWFuLWpzZHJhdy1wbHVnaW4vdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyByZXNvbHZlIH0gZnJvbSBcInBhdGhcIlxuaW1wb3J0IHsgZGVmaW5lQ29uZmlnLCBsb2FkRW52IH0gZnJvbSBcInZpdGVcIlxuaW1wb3J0IHsgdml0ZVN0YXRpY0NvcHkgfSBmcm9tIFwidml0ZS1wbHVnaW4tc3RhdGljLWNvcHlcIlxuaW1wb3J0IGxpdmVyZWxvYWQgZnJvbSBcInJvbGx1cC1wbHVnaW4tbGl2ZXJlbG9hZFwiXG5pbXBvcnQgeyBzdmVsdGUgfSBmcm9tIFwiQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZVwiXG5pbXBvcnQgemlwUGFjayBmcm9tIFwidml0ZS1wbHVnaW4temlwLXBhY2tcIjtcbmltcG9ydCBmZyBmcm9tICdmYXN0LWdsb2InO1xuXG5pbXBvcnQgdml0ZVBsdWdpbllhbWxJMThuIGZyb20gJy4veWFtbC1wbHVnaW4nO1xuXG5jb25zdCBlbnYgPSBwcm9jZXNzLmVudjtcbmNvbnN0IGlzU3JjbWFwID0gZW52LlZJVEVfU09VUkNFTUFQID09PSAnaW5saW5lJztcbmNvbnN0IGlzRGV2ID0gZW52Lk5PREVfRU5WID09PSAnZGV2ZWxvcG1lbnQnO1xuXG5jb25zdCBvdXRwdXREaXIgPSBpc0RldiA/IFwiZGV2XCIgOiBcImRpc3RcIjtcblxuY29uc29sZS5sb2coXCJpc0Rldj0+XCIsIGlzRGV2KTtcbmNvbnNvbGUubG9nKFwiaXNTcmNtYXA9PlwiLCBpc1NyY21hcCk7XG5jb25zb2xlLmxvZyhcIm91dHB1dERpcj0+XCIsIG91dHB1dERpcik7XG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gICAgcmVzb2x2ZToge1xuICAgICAgICBhbGlhczoge1xuICAgICAgICAgICAgXCJAXCI6IHJlc29sdmUoX19kaXJuYW1lLCBcInNyY1wiKSxcbiAgICAgICAgfVxuICAgIH0sXG5cbiAgICBwbHVnaW5zOiBbXG4gICAgICAgIHN2ZWx0ZSgpLFxuXG4gICAgICAgIHZpdGVQbHVnaW5ZYW1sSTE4bih7XG4gICAgICAgICAgICBpbkRpcjogJ3B1YmxpYy9pMThuJyxcbiAgICAgICAgICAgIG91dERpcjogYCR7b3V0cHV0RGlyfS9pMThuYFxuICAgICAgICB9KSxcblxuICAgICAgICB2aXRlU3RhdGljQ29weSh7XG4gICAgICAgICAgICB0YXJnZXRzOiBbXG4gICAgICAgICAgICAgICAgeyBzcmM6IFwiLi9SRUFETUUqLm1kXCIsIGRlc3Q6IFwiLi9cIiB9LFxuICAgICAgICAgICAgICAgIHsgc3JjOiBcIi4vcGx1Z2luLmpzb25cIiwgZGVzdDogXCIuL1wiIH0sXG4gICAgICAgICAgICAgICAgeyBzcmM6IFwiLi9wcmV2aWV3LnBuZ1wiLCBkZXN0OiBcIi4vXCIgfSxcbiAgICAgICAgICAgICAgICB7IHNyYzogXCIuL2ljb24ucG5nXCIsIGRlc3Q6IFwiLi9cIiB9XG4gICAgICAgICAgICBdLFxuICAgICAgICB9KSxcblxuICAgIF0sXG5cbiAgICBkZWZpbmU6IHtcbiAgICAgICAgXCJwcm9jZXNzLmVudi5ERVZfTU9ERVwiOiBKU09OLnN0cmluZ2lmeShpc0RldiksXG4gICAgICAgIFwicHJvY2Vzcy5lbnYuTk9ERV9FTlZcIjogSlNPTi5zdHJpbmdpZnkoZW52Lk5PREVfRU5WKVxuICAgIH0sXG5cbiAgICBidWlsZDoge1xuICAgICAgICBvdXREaXI6IG91dHB1dERpcixcbiAgICAgICAgZW1wdHlPdXREaXI6IGZhbHNlLFxuICAgICAgICBtaW5pZnk6IHRydWUsXG4gICAgICAgIHNvdXJjZW1hcDogaXNTcmNtYXAgPyAnaW5saW5lJyA6IGZhbHNlLFxuXG4gICAgICAgIGxpYjoge1xuICAgICAgICAgICAgZW50cnk6IHJlc29sdmUoX19kaXJuYW1lLCBcInNyYy9pbmRleC50c1wiKSxcbiAgICAgICAgICAgIGZpbGVOYW1lOiBcImluZGV4XCIsXG4gICAgICAgICAgICBmb3JtYXRzOiBbXCJjanNcIl0sXG4gICAgICAgIH0sXG4gICAgICAgIHJvbGx1cE9wdGlvbnM6IHtcbiAgICAgICAgICAgIHBsdWdpbnM6IFtcbiAgICAgICAgICAgICAgICAuLi4oaXNEZXYgPyBbXG4gICAgICAgICAgICAgICAgICAgIGxpdmVyZWxvYWQob3V0cHV0RGlyKSxcbiAgICAgICAgICAgICAgICAgICAge1xuICAgICAgICAgICAgICAgICAgICAgICAgbmFtZTogJ3dhdGNoLWV4dGVybmFsJyxcbiAgICAgICAgICAgICAgICAgICAgICAgIGFzeW5jIGJ1aWxkU3RhcnQoKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgY29uc3QgZmlsZXMgPSBhd2FpdCBmZyhbXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICdwdWJsaWMvaTE4bi8qKicsXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICcuL1JFQURNRSoubWQnLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnLi9wbHVnaW4uanNvbidcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBdKTtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBmb3IgKGxldCBmaWxlIG9mIGZpbGVzKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMuYWRkV2F0Y2hGaWxlKGZpbGUpO1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIF0gOiBbXG4gICAgICAgICAgICAgICAgICAgIC8vIENsZWFuIHVwIHVubmVjZXNzYXJ5IGZpbGVzIHVuZGVyIGRpc3QgZGlyXG4gICAgICAgICAgICAgICAgICAgIGNsZWFudXBEaXN0RmlsZXMoe1xuICAgICAgICAgICAgICAgICAgICAgICAgcGF0dGVybnM6IFsnaTE4bi8qLnlhbWwnLCAnaTE4bi8qLm1kJ10sXG4gICAgICAgICAgICAgICAgICAgICAgICBkaXN0RGlyOiBvdXRwdXREaXJcbiAgICAgICAgICAgICAgICAgICAgfSksXG4gICAgICAgICAgICAgICAgICAgIHppcFBhY2soe1xuICAgICAgICAgICAgICAgICAgICAgICAgaW5EaXI6ICcuL2Rpc3QnLFxuICAgICAgICAgICAgICAgICAgICAgICAgb3V0RGlyOiAnLi8nLFxuICAgICAgICAgICAgICAgICAgICAgICAgb3V0RmlsZU5hbWU6ICdwYWNrYWdlLnppcCdcbiAgICAgICAgICAgICAgICAgICAgfSlcbiAgICAgICAgICAgICAgICBdKVxuICAgICAgICAgICAgXSxcblxuICAgICAgICAgICAgZXh0ZXJuYWw6IFtcInNpeXVhblwiLCBcInByb2Nlc3NcIl0sXG5cbiAgICAgICAgICAgIG91dHB1dDoge1xuICAgICAgICAgICAgICAgIGVudHJ5RmlsZU5hbWVzOiBcIltuYW1lXS5qc1wiLFxuICAgICAgICAgICAgICAgIGFzc2V0RmlsZU5hbWVzOiAoYXNzZXRJbmZvKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgIGlmIChhc3NldEluZm8ubmFtZSA9PT0gXCJzdHlsZS5jc3NcIikge1xuICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuIFwiaW5kZXguY3NzXCJcbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgICAgICByZXR1cm4gYXNzZXRJbmZvLm5hbWVcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgfSxcbiAgICAgICAgfSxcbiAgICB9XG59KTtcblxuXG4vKipcbiAqIENsZWFuIHVwIHNvbWUgZGlzdCBmaWxlcyBhZnRlciBjb21waWxlZFxuICogQGF1dGhvciBmcm9zdGltZVxuICogQHBhcmFtIG9wdGlvbnM6XG4gKiBAcmV0dXJucyBcbiAqL1xuZnVuY3Rpb24gY2xlYW51cERpc3RGaWxlcyhvcHRpb25zOiB7IHBhdHRlcm5zOiBzdHJpbmdbXSwgZGlzdERpcjogc3RyaW5nIH0pIHtcbiAgICBjb25zdCB7XG4gICAgICAgIHBhdHRlcm5zLFxuICAgICAgICBkaXN0RGlyXG4gICAgfSA9IG9wdGlvbnM7XG5cbiAgICByZXR1cm4ge1xuICAgICAgICBuYW1lOiAncm9sbHVwLXBsdWdpbi1jbGVhbnVwJyxcbiAgICAgICAgZW5mb3JjZTogJ3Bvc3QnLFxuICAgICAgICB3cml0ZUJ1bmRsZToge1xuICAgICAgICAgICAgc2VxdWVudGlhbDogdHJ1ZSxcbiAgICAgICAgICAgIG9yZGVyOiAncG9zdCcgYXMgJ3Bvc3QnLFxuICAgICAgICAgICAgYXN5bmMgaGFuZGxlcigpIHtcbiAgICAgICAgICAgICAgICBjb25zdCBmZyA9IGF3YWl0IGltcG9ydCgnZmFzdC1nbG9iJyk7XG4gICAgICAgICAgICAgICAgY29uc3QgZnMgPSBhd2FpdCBpbXBvcnQoJ2ZzJyk7XG4gICAgICAgICAgICAgICAgLy8gY29uc3QgcGF0aCA9IGF3YWl0IGltcG9ydCgncGF0aCcpO1xuXG4gICAgICAgICAgICAgICAgLy8gXHU0RjdGXHU3NTI4IGdsb2IgXHU4QkVEXHU2Q0Q1XHVGRjBDXHU3ODZFXHU0RkREXHU4MEZEXHU1MzM5XHU5MTREXHU1MjMwXHU2NTg3XHU0RUY2XG4gICAgICAgICAgICAgICAgY29uc3QgZGlzdFBhdHRlcm5zID0gcGF0dGVybnMubWFwKHBhdCA9PiBgJHtkaXN0RGlyfS8ke3BhdH1gKTtcbiAgICAgICAgICAgICAgICBjb25zb2xlLmRlYnVnKCdDbGVhbnVwIHNlYXJjaGluZyBwYXR0ZXJuczonLCBkaXN0UGF0dGVybnMpO1xuXG4gICAgICAgICAgICAgICAgY29uc3QgZmlsZXMgPSBhd2FpdCBmZy5kZWZhdWx0KGRpc3RQYXR0ZXJucywge1xuICAgICAgICAgICAgICAgICAgICBkb3Q6IHRydWUsXG4gICAgICAgICAgICAgICAgICAgIGFic29sdXRlOiB0cnVlLFxuICAgICAgICAgICAgICAgICAgICBvbmx5RmlsZXM6IGZhbHNlXG4gICAgICAgICAgICAgICAgfSk7XG5cbiAgICAgICAgICAgICAgICAvLyBjb25zb2xlLmluZm8oJ0ZpbGVzIHRvIGJlIGNsZWFuZWQgdXA6JywgZmlsZXMpO1xuXG4gICAgICAgICAgICAgICAgZm9yIChjb25zdCBmaWxlIG9mIGZpbGVzKSB7XG4gICAgICAgICAgICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICAgICAgICAgICAgICBpZiAoZnMuZGVmYXVsdC5leGlzdHNTeW5jKGZpbGUpKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgY29uc3Qgc3RhdCA9IGZzLmRlZmF1bHQuc3RhdFN5bmMoZmlsZSk7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgaWYgKHN0YXQuaXNEaXJlY3RvcnkoKSkge1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmcy5kZWZhdWx0LnJtU3luYyhmaWxlLCB7IHJlY3Vyc2l2ZTogdHJ1ZSB9KTtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmcy5kZWZhdWx0LnVubGlua1N5bmMoZmlsZSk7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbnNvbGUubG9nKGBDbGVhbmVkIHVwOiAke2ZpbGV9YCk7XG4gICAgICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAgIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zb2xlLmVycm9yKGBGYWlsZWQgdG8gY2xlYW4gdXAgJHtmaWxlfTpgLCBlcnJvcik7XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICB9O1xufVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS9tYXNzaXZlL0Rldi9zaXl1YW4tanNkcmF3LXBsdWdpblwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL2hvbWUvbWFzc2l2ZS9EZXYvc2l5dWFuLWpzZHJhdy1wbHVnaW4veWFtbC1wbHVnaW4uanNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2hvbWUvbWFzc2l2ZS9EZXYvc2l5dWFuLWpzZHJhdy1wbHVnaW4veWFtbC1wbHVnaW4uanNcIjsvKlxuICogQ29weXJpZ2h0IChjKSAyMDI0IGJ5IGZyb3N0aW1lLiBBbGwgUmlnaHRzIFJlc2VydmVkLlxuICogQEF1dGhvciAgICAgICA6IGZyb3N0aW1lXG4gKiBARGF0ZSAgICAgICAgIDogMjAyNC0wNC0wNSAyMToyNzo1NVxuICogQEZpbGVQYXRoICAgICA6IC95YW1sLXBsdWdpbi5qc1xuICogQExhc3RFZGl0VGltZSA6IDIwMjQtMDQtMDUgMjI6NTM6MzRcbiAqIEBEZXNjcmlwdGlvbiAgOiBcdTUzQkJcdTU5QUVcdTczOUJcdTc2ODQganNvbiBcdTY4M0NcdTVGMEZcdUZGMENcdTYyMTFcdTVDMzFcdTY2MkZcdTg5ODFcdTc1MjggeWFtbCBcdTUxOTkgaTE4blxuICovXG4vLyBwbHVnaW5zL3ZpdGUtcGx1Z2luLXBhcnNlLXlhbWwuanNcbmltcG9ydCBmcyBmcm9tICdmcyc7XG5pbXBvcnQgeWFtbCBmcm9tICdqcy15YW1sJztcbmltcG9ydCB7IHJlc29sdmUgfSBmcm9tICdwYXRoJztcblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gdml0ZVBsdWdpbllhbWxJMThuKG9wdGlvbnMgPSB7fSkge1xuICAgIC8vIERlZmF1bHQgb3B0aW9ucyB3aXRoIGEgZmFsbGJhY2tcbiAgICBjb25zdCBEZWZhdWx0T3B0aW9ucyA9IHtcbiAgICAgICAgaW5EaXI6ICdzcmMvaTE4bicsXG4gICAgICAgIG91dERpcjogJ2Rpc3QvaTE4bicsXG4gICAgfTtcblxuICAgIGNvbnN0IGZpbmFsT3B0aW9ucyA9IHsgLi4uRGVmYXVsdE9wdGlvbnMsIC4uLm9wdGlvbnMgfTtcblxuICAgIHJldHVybiB7XG4gICAgICAgIG5hbWU6ICd2aXRlLXBsdWdpbi15YW1sLWkxOG4nLFxuICAgICAgICBidWlsZFN0YXJ0KCkge1xuICAgICAgICAgICAgY29uc29sZS5sb2coJ1x1RDgzQ1x1REYwOCBQYXJzZSBJMThuOiBZQU1MIHRvIEpTT04uLicpO1xuICAgICAgICAgICAgY29uc3QgaW5EaXIgPSBmaW5hbE9wdGlvbnMuaW5EaXI7XG4gICAgICAgICAgICBjb25zdCBvdXREaXIgPSBmaW5hbE9wdGlvbnMub3V0RGlyXG5cbiAgICAgICAgICAgIGlmICghZnMuZXhpc3RzU3luYyhvdXREaXIpKSB7XG4gICAgICAgICAgICAgICAgZnMubWtkaXJTeW5jKG91dERpciwgeyByZWN1cnNpdmU6IHRydWUgfSk7XG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgIC8vUGFyc2UgeWFtbCBmaWxlLCBvdXRwdXQgdG8ganNvblxuICAgICAgICAgICAgY29uc3QgZmlsZXMgPSBmcy5yZWFkZGlyU3luYyhpbkRpcik7XG4gICAgICAgICAgICBmb3IgKGNvbnN0IGZpbGUgb2YgZmlsZXMpIHtcbiAgICAgICAgICAgICAgICBpZiAoZmlsZS5lbmRzV2l0aCgnLnlhbWwnKSB8fCBmaWxlLmVuZHNXaXRoKCcueW1sJykpIHtcbiAgICAgICAgICAgICAgICAgICAgY29uc29sZS5sb2coYC0tIFBhcnNpbmcgJHtmaWxlfWApXG4gICAgICAgICAgICAgICAgICAgIC8vXHU2OEMwXHU2N0U1XHU2NjJGXHU1NDI2XHU2NzA5XHU1NDBDXHU1NDBEXHU3Njg0anNvblx1NjU4N1x1NEVGNlxuICAgICAgICAgICAgICAgICAgICBjb25zdCBqc29uRmlsZSA9IGZpbGUucmVwbGFjZSgvXFwuKHlhbWx8eW1sKSQvLCAnLmpzb24nKTtcbiAgICAgICAgICAgICAgICAgICAgaWYgKGZpbGVzLmluY2x1ZGVzKGpzb25GaWxlKSkge1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc29sZS5sb2coYC0tLS0gRmlsZSAke2pzb25GaWxlfSBhbHJlYWR5IGV4aXN0cywgc2tpcHBpbmcuLi5gKTtcbiAgICAgICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlO1xuICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zdCBmaWxlUGF0aCA9IHJlc29sdmUoaW5EaXIsIGZpbGUpO1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc3QgZmlsZUNvbnRlbnRzID0gZnMucmVhZEZpbGVTeW5jKGZpbGVQYXRoLCAndXRmOCcpO1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc3QgcGFyc2VkID0geWFtbC5sb2FkKGZpbGVDb250ZW50cyk7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zdCBqc29uQ29udGVudCA9IEpTT04uc3RyaW5naWZ5KHBhcnNlZCwgbnVsbCwgMik7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zdCBvdXRwdXRGaWxlUGF0aCA9IHJlc29sdmUob3V0RGlyLCBmaWxlLnJlcGxhY2UoL1xcLih5YW1sfHltbCkkLywgJy5qc29uJykpO1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc29sZS5sb2coYC0tLS0gV3JpdGluZyB0byAke291dHB1dEZpbGVQYXRofWApO1xuICAgICAgICAgICAgICAgICAgICAgICAgZnMud3JpdGVGaWxlU3luYyhvdXRwdXRGaWxlUGF0aCwganNvbkNvbnRlbnQpO1xuICAgICAgICAgICAgICAgICAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5lcnJvcihgLS0tLSBFcnJvciBwYXJzaW5nIFlBTUwgZmlsZSAke2ZpbGV9OiAke2Vycm9yLm1lc3NhZ2V9YCk7XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgfTtcbn1cbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBb1MsU0FBUyxXQUFBQSxnQkFBZTtBQUM1VCxTQUFTLG9CQUE2QjtBQUN0QyxTQUFTLHNCQUFzQjtBQUMvQixPQUFPLGdCQUFnQjtBQUN2QixTQUFTLGNBQWM7QUFDdkIsT0FBTyxhQUFhO0FBQ3BCLE9BQU8sUUFBUTs7O0FDR2YsT0FBTyxRQUFRO0FBQ2YsT0FBTyxVQUFVO0FBQ2pCLFNBQVMsZUFBZTtBQUVULFNBQVIsbUJBQW9DLFVBQVUsQ0FBQyxHQUFHO0FBRXJELFFBQU0saUJBQWlCO0FBQUEsSUFDbkIsT0FBTztBQUFBLElBQ1AsUUFBUTtBQUFBLEVBQ1o7QUFFQSxRQUFNLGVBQWUsRUFBRSxHQUFHLGdCQUFnQixHQUFHLFFBQVE7QUFFckQsU0FBTztBQUFBLElBQ0gsTUFBTTtBQUFBLElBQ04sYUFBYTtBQUNULGNBQVEsSUFBSSxzQ0FBK0I7QUFDM0MsWUFBTSxRQUFRLGFBQWE7QUFDM0IsWUFBTSxTQUFTLGFBQWE7QUFFNUIsVUFBSSxDQUFDLEdBQUcsV0FBVyxNQUFNLEdBQUc7QUFDeEIsV0FBRyxVQUFVLFFBQVEsRUFBRSxXQUFXLEtBQUssQ0FBQztBQUFBLE1BQzVDO0FBR0EsWUFBTSxRQUFRLEdBQUcsWUFBWSxLQUFLO0FBQ2xDLGlCQUFXLFFBQVEsT0FBTztBQUN0QixZQUFJLEtBQUssU0FBUyxPQUFPLEtBQUssS0FBSyxTQUFTLE1BQU0sR0FBRztBQUNqRCxrQkFBUSxJQUFJLGNBQWMsSUFBSSxFQUFFO0FBRWhDLGdCQUFNLFdBQVcsS0FBSyxRQUFRLGlCQUFpQixPQUFPO0FBQ3RELGNBQUksTUFBTSxTQUFTLFFBQVEsR0FBRztBQUMxQixvQkFBUSxJQUFJLGFBQWEsUUFBUSw4QkFBOEI7QUFDL0Q7QUFBQSxVQUNKO0FBQ0EsY0FBSTtBQUNBLGtCQUFNLFdBQVcsUUFBUSxPQUFPLElBQUk7QUFDcEMsa0JBQU0sZUFBZSxHQUFHLGFBQWEsVUFBVSxNQUFNO0FBQ3JELGtCQUFNLFNBQVMsS0FBSyxLQUFLLFlBQVk7QUFDckMsa0JBQU0sY0FBYyxLQUFLLFVBQVUsUUFBUSxNQUFNLENBQUM7QUFDbEQsa0JBQU0saUJBQWlCLFFBQVEsUUFBUSxLQUFLLFFBQVEsaUJBQWlCLE9BQU8sQ0FBQztBQUM3RSxvQkFBUSxJQUFJLG1CQUFtQixjQUFjLEVBQUU7QUFDL0MsZUFBRyxjQUFjLGdCQUFnQixXQUFXO0FBQUEsVUFDaEQsU0FBUyxPQUFPO0FBQ1osaUJBQUssTUFBTSxnQ0FBZ0MsSUFBSSxLQUFLLE1BQU0sT0FBTyxFQUFFO0FBQUEsVUFDdkU7QUFBQSxRQUNKO0FBQUEsTUFDSjtBQUFBLElBQ0o7QUFBQSxFQUNKO0FBQ0o7OztBRDNEQSxJQUFNLG1DQUFtQztBQVV6QyxJQUFNLE1BQU0sUUFBUTtBQUNwQixJQUFNLFdBQVcsSUFBSSxtQkFBbUI7QUFDeEMsSUFBTSxRQUFRLElBQUksYUFBYTtBQUUvQixJQUFNLFlBQVksUUFBUSxRQUFRO0FBRWxDLFFBQVEsSUFBSSxXQUFXLEtBQUs7QUFDNUIsUUFBUSxJQUFJLGNBQWMsUUFBUTtBQUNsQyxRQUFRLElBQUksZUFBZSxTQUFTO0FBRXBDLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQ3hCLFNBQVM7QUFBQSxJQUNMLE9BQU87QUFBQSxNQUNILEtBQUtDLFNBQVEsa0NBQVcsS0FBSztBQUFBLElBQ2pDO0FBQUEsRUFDSjtBQUFBLEVBRUEsU0FBUztBQUFBLElBQ0wsT0FBTztBQUFBLElBRVAsbUJBQW1CO0FBQUEsTUFDZixPQUFPO0FBQUEsTUFDUCxRQUFRLEdBQUcsU0FBUztBQUFBLElBQ3hCLENBQUM7QUFBQSxJQUVELGVBQWU7QUFBQSxNQUNYLFNBQVM7QUFBQSxRQUNMLEVBQUUsS0FBSyxnQkFBZ0IsTUFBTSxLQUFLO0FBQUEsUUFDbEMsRUFBRSxLQUFLLGlCQUFpQixNQUFNLEtBQUs7QUFBQSxRQUNuQyxFQUFFLEtBQUssaUJBQWlCLE1BQU0sS0FBSztBQUFBLFFBQ25DLEVBQUUsS0FBSyxjQUFjLE1BQU0sS0FBSztBQUFBLE1BQ3BDO0FBQUEsSUFDSixDQUFDO0FBQUEsRUFFTDtBQUFBLEVBRUEsUUFBUTtBQUFBLElBQ0osd0JBQXdCLEtBQUssVUFBVSxLQUFLO0FBQUEsSUFDNUMsd0JBQXdCLEtBQUssVUFBVSxJQUFJLFFBQVE7QUFBQSxFQUN2RDtBQUFBLEVBRUEsT0FBTztBQUFBLElBQ0gsUUFBUTtBQUFBLElBQ1IsYUFBYTtBQUFBLElBQ2IsUUFBUTtBQUFBLElBQ1IsV0FBVyxXQUFXLFdBQVc7QUFBQSxJQUVqQyxLQUFLO0FBQUEsTUFDRCxPQUFPQSxTQUFRLGtDQUFXLGNBQWM7QUFBQSxNQUN4QyxVQUFVO0FBQUEsTUFDVixTQUFTLENBQUMsS0FBSztBQUFBLElBQ25CO0FBQUEsSUFDQSxlQUFlO0FBQUEsTUFDWCxTQUFTO0FBQUEsUUFDTCxHQUFJLFFBQVE7QUFBQSxVQUNSLFdBQVcsU0FBUztBQUFBLFVBQ3BCO0FBQUEsWUFDSSxNQUFNO0FBQUEsWUFDTixNQUFNLGFBQWE7QUFDZixvQkFBTSxRQUFRLE1BQU0sR0FBRztBQUFBLGdCQUNuQjtBQUFBLGdCQUNBO0FBQUEsZ0JBQ0E7QUFBQSxjQUNKLENBQUM7QUFDRCx1QkFBUyxRQUFRLE9BQU87QUFDcEIscUJBQUssYUFBYSxJQUFJO0FBQUEsY0FDMUI7QUFBQSxZQUNKO0FBQUEsVUFDSjtBQUFBLFFBQ0osSUFBSTtBQUFBO0FBQUEsVUFFQSxpQkFBaUI7QUFBQSxZQUNiLFVBQVUsQ0FBQyxlQUFlLFdBQVc7QUFBQSxZQUNyQyxTQUFTO0FBQUEsVUFDYixDQUFDO0FBQUEsVUFDRCxRQUFRO0FBQUEsWUFDSixPQUFPO0FBQUEsWUFDUCxRQUFRO0FBQUEsWUFDUixhQUFhO0FBQUEsVUFDakIsQ0FBQztBQUFBLFFBQ0w7QUFBQSxNQUNKO0FBQUEsTUFFQSxVQUFVLENBQUMsVUFBVSxTQUFTO0FBQUEsTUFFOUIsUUFBUTtBQUFBLFFBQ0osZ0JBQWdCO0FBQUEsUUFDaEIsZ0JBQWdCLENBQUMsY0FBYztBQUMzQixjQUFJLFVBQVUsU0FBUyxhQUFhO0FBQ2hDLG1CQUFPO0FBQUEsVUFDWDtBQUNBLGlCQUFPLFVBQVU7QUFBQSxRQUNyQjtBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQUEsRUFDSjtBQUNKLENBQUM7QUFTRCxTQUFTLGlCQUFpQixTQUFrRDtBQUN4RSxRQUFNO0FBQUEsSUFDRjtBQUFBLElBQ0E7QUFBQSxFQUNKLElBQUk7QUFFSixTQUFPO0FBQUEsSUFDSCxNQUFNO0FBQUEsSUFDTixTQUFTO0FBQUEsSUFDVCxhQUFhO0FBQUEsTUFDVCxZQUFZO0FBQUEsTUFDWixPQUFPO0FBQUEsTUFDUCxNQUFNLFVBQVU7QUFDWixjQUFNQyxNQUFLLE1BQU0sT0FBTyxtRkFBVztBQUNuQyxjQUFNQyxNQUFLLE1BQU0sT0FBTyxJQUFJO0FBSTVCLGNBQU0sZUFBZSxTQUFTLElBQUksU0FBTyxHQUFHLE9BQU8sSUFBSSxHQUFHLEVBQUU7QUFDNUQsZ0JBQVEsTUFBTSwrQkFBK0IsWUFBWTtBQUV6RCxjQUFNLFFBQVEsTUFBTUQsSUFBRyxRQUFRLGNBQWM7QUFBQSxVQUN6QyxLQUFLO0FBQUEsVUFDTCxVQUFVO0FBQUEsVUFDVixXQUFXO0FBQUEsUUFDZixDQUFDO0FBSUQsbUJBQVcsUUFBUSxPQUFPO0FBQ3RCLGNBQUk7QUFDQSxnQkFBSUMsSUFBRyxRQUFRLFdBQVcsSUFBSSxHQUFHO0FBQzdCLG9CQUFNLE9BQU9BLElBQUcsUUFBUSxTQUFTLElBQUk7QUFDckMsa0JBQUksS0FBSyxZQUFZLEdBQUc7QUFDcEIsZ0JBQUFBLElBQUcsUUFBUSxPQUFPLE1BQU0sRUFBRSxXQUFXLEtBQUssQ0FBQztBQUFBLGNBQy9DLE9BQU87QUFDSCxnQkFBQUEsSUFBRyxRQUFRLFdBQVcsSUFBSTtBQUFBLGNBQzlCO0FBQ0Esc0JBQVEsSUFBSSxlQUFlLElBQUksRUFBRTtBQUFBLFlBQ3JDO0FBQUEsVUFDSixTQUFTLE9BQU87QUFDWixvQkFBUSxNQUFNLHNCQUFzQixJQUFJLEtBQUssS0FBSztBQUFBLFVBQ3REO0FBQUEsUUFDSjtBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQUEsRUFDSjtBQUNKOyIsCiAgIm5hbWVzIjogWyJyZXNvbHZlIiwgInJlc29sdmUiLCAiZmciLCAiZnMiXQp9Cg==