diff --git a/.github/workflows/release.yml b/.forgejo/workflows/build.yml similarity index 60% rename from .github/workflows/release.yml rename to .forgejo/workflows/build.yml index 49834e5..4bfcc57 100644 --- a/.github/workflows/release.yml +++ b/.forgejo/workflows/build.yml @@ -1,7 +1,9 @@ -name: Create Release on Tag Push +name: Build on Push and create Release on Tag on: push: + branches: + - main tags: - "v*" @@ -20,7 +22,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 @@ -28,6 +30,12 @@ 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 @@ -52,11 +60,22 @@ jobs: - name: Build for production run: pnpm build - - name: Release - uses: ncipollo/release-action@v1 + # Move file + - name: Move file + run: mkdir built; mv package.zip built/package.zip + + # Upload artifacts + - name: Upload artifacts + uses: actions/upload-artifact@v3 with: - allowUpdates: true - artifactErrorsFailBuild: true - artifacts: "package.zip" - token: ${{ secrets.GITHUB_TOKEN }} - prerelease: false + 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 }} diff --git a/README.md b/README.md index 73c10ff..258f071 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,23 @@ # 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 -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. +- Install the plugin from the marketplace. You can find it by searching for `js-draw`. +- To add a new whiteboard to your document: + 1. Type `/Insert whiteboard` 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 whiteboard later: + 1. Left-click or tap on the whiteboard to select it, then click on the Edit icon in the top bar + - Or right-click on the whiteboard (or click the three dots on mobile), select "Plugin" > "Edit whiteboard" in the menu + 2. The editor tab will open, edit your file as you like, then click the Save button and close the tab. ## Planned features -- [ ] 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! +Check out the [Projects](https://git.massive.box/massivebox/siyuan-jsdraw-plugin/projects) tab! ## 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 78f409d..b4b2293 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "plugin-sample-vite-svelte", - "version": "0.3.6", + "name": "siyuan-jsdraw-plugin", + "version": "0.4.1", "type": "module", - "description": "This is a sample plugin based on vite and svelte for Siyuan (https://b3log.org/siyuan)", - "repository": "", - "homepage": "", - "author": "frostime", + "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", "license": "MIT", "scripts": { "dev": "cross-env NODE_ENV=development VITE_SOURCEMAP=inline vite build --watch", @@ -36,6 +36,7 @@ }, "dependencies": { "@js-draw/material-icons": "^1.29.0", - "js-draw": "^1.29.0" + "js-draw": "^1.29.0", + "ts-serializable": "^4.2.0" } } diff --git a/plugin.json b/plugin.json index 8413186..a5431f9 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.1.0", + "version": "0.4.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 new file mode 100644 index 0000000..1513654 --- /dev/null +++ b/public/i18n/en_US.json @@ -0,0 +1,50 @@ +{ + "insertWhiteboard": "Insert whiteboard", + "editWhiteboard": "Edit whiteboard", + "editShortcut": "Edit selected whiteboard", + "errNoFileID": "File ID missing - couldn't open file.", + "errNotAWhiteboard": "You must select a whiteboard, not a regular image. Usage instructions", + "errSyncIDNotFound": "Couldn't find SyncID in document for drawing, make sure you're trying to edit a whiteboard 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.", + "errMultipleSyncIDs": "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.\nFile IDs (the part you can change in the Rename menu) must be unique across all documents.\nFull explanation", + "errUnchangedProtyle": "Make sure the image you're trying to edit still exists in your documents.", + "errSaveGeneric": "Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there.", + "errMustSelect": "Select a whiteboard in your document by left-clicking it, then use this icon/shortcut to open the editor directly. Usage instructions", + "whiteboard": "Whiteboard", + "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 whiteboards." + }, + "backgroundDropdown":{ + "title": "Background color", + "description": "Default background color for new whiteboards." + }, + "background": { + "title": "Custom background", + "description": "Hexadecimal code of the custom background color for new whiteboards.
This setting is only applied if \"Background Color\" is set to \"Custom\"!" + }, + "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).
The editor will always open as a dialog on mobile." + }, + "analytics": { + "title": "Analytics", + "description": "Enable to send anonymous usage data to the developer. Privacy Policy" + }, + "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 same whiteboard." + } + } +} \ No newline at end of file diff --git a/public/webapp/button.js b/public/webapp/button.js index 610799d..8be8fd4 100644 --- a/public/webapp/button.js +++ b/public/webapp/button.js @@ -1,12 +1,15 @@ -function copyEditLink(fileID) { - navigator.clipboard.writeText(getEditLink(fileID)); +function copyEditLink(path) { + navigator.clipboard.writeText(getEditLink(path)); +} +function copyImageLink(path) { + navigator.clipboard.writeText(`![Drawing](${path})`); } function refreshPage() { window.location.reload(); } -function addButton(document, fileID) { +function addButton(document, path) { // Add floating button const floatingButton = document.createElement('button'); @@ -19,8 +22,8 @@ function addButton(document, fileID) { popupMenu.id = 'popupMenu'; popupMenu.innerHTML = ` - - + + `; document.body.appendChild(popupMenu); @@ -31,6 +34,7 @@ function addButton(document, fileID) { 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 new file mode 100644 index 0000000..939ddde Binary files /dev/null and b/public/webapp/cursor.png differ diff --git a/public/webapp/draw.js b/public/webapp/draw.js index 9d9adfc..187dab2 100644 --- a/public/webapp/draw.js +++ b/public/webapp/draw.js @@ -27,9 +27,9 @@ async function getFile(path) { } -async function getSVG(fileID) { +async function getSVG(path) { - const resp = await getFile("/data/assets/" + fileID + '.svg'); + const resp = await getFile("/data/" + path); if(resp == null) { return FALLBACK; } @@ -37,10 +37,10 @@ async function getSVG(fileID) { } -function getEditLink(fileID) { +function getEditLink(path) { const data = encodeURIComponent( JSON.stringify({ - id: fileID + path: path, }) ) return `siyuan://plugins/siyuan-jsdraw-pluginwhiteboard/?icon=iconDraw&title=Drawing&data=${data}`; diff --git a/public/webapp/error.html b/public/webapp/error.html new file mode 100644 index 0000000..2281841 --- /dev/null +++ b/public/webapp/error.html @@ -0,0 +1,21 @@ + + + + Error + + + +

It looks like an error occurred. You shouldn't be able to see this page.

+

No data has been deleted. Please excuse us for the inconvenience.

+

+ Try reloading SiYuan, and if the error persists, open an issue at + https://git.massive.box/massivebox/siyuan-jsdraw-plugin/issues + or contact the developer directly via e-mail at box@massive.box +

+ + \ No newline at end of file diff --git a/public/webapp/index.html b/public/webapp/index.html index a1fd2f6..9a02454 100644 --- a/public/webapp/index.html +++ b/public/webapp/index.html @@ -5,18 +5,22 @@ diff --git a/scripts/validate_tag.cjs b/scripts/validate_tag.cjs new file mode 100644 index 0000000..c842ffc --- /dev/null +++ b/scripts/validate_tag.cjs @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..13d37be --- /dev/null +++ b/src/analytics.ts @@ -0,0 +1,46 @@ +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 new file mode 100644 index 0000000..116a1af --- /dev/null +++ b/src/config.ts @@ -0,0 +1,178 @@ +import {PluginFile} from "@/file"; +import {CONFIG_FILENAME, JSON_MIME, STORAGE_PATH} from "@/const"; +import {Plugin} from "siyuan"; +import {SettingUtils} from "@/libs/setting-utils"; +import {getFirstDefined} from "@/helper"; +import {ErrorReporter, InvalidBackgroundColorError} from "@/errors"; + +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)) { + ErrorReporter.error(new InvalidBackgroundColorError()); + 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 1acaa80..b7d60aa 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,7 +1,9 @@ export const SVG_MIME = "image/svg+xml"; export const JSON_MIME = "application/json"; -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 +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 diff --git a/src/editor.ts b/src/editor.ts new file mode 100644 index 0000000..8fbeefb --- /dev/null +++ b/src/editor.ts @@ -0,0 +1,247 @@ +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} from "siyuan"; +import {findSyncIDInProtyle, replaceSyncID} from "@/protyle"; +import DrawJSPlugin from "@/index"; +import {EditorOptions} from "@/config"; +import 'js-draw/styles'; +import { + ErrorReporter, + GenericSaveError, InternationalizedError, NoFileIDError, 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') 3 3, none; + } + `; + 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 { + + const instance = new PluginEditor(fileID); + + await instance.genToolbar(); + let syncID = await findSyncIDInProtyle(fileID); + + if(syncID == null) { + throw new SyncIDNotFoundError(); + } + 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) { + if(error instanceof InternationalizedError) { + ErrorReporter.error(error); + }else{ + ErrorReporter.error(new GenericSaveError()); + console.error(error); + } + await navigator.clipboard.writeText(svgElem.outerHTML); + 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) { + ErrorReporter.error(error); + } + return instance; + } + + static registerTab(p: DrawJSPlugin) { + p.addTab({ + 'type': "whiteboard", + async init() { + const fileID = this.data.fileID; + if (fileID == null) { + ErrorReporter.error(new NoFileIDError()); + return; + } + try { + const editor = await PluginEditor.create(fileID, p.config.options.editorOptions); + this.element.appendChild(editor.getElement()); + }catch (error){ + ErrorReporter.error(error); + } + } + }); + } + + toTab(p: Plugin) { + openTab({ + app: p.app, + custom: { + title: p.i18n.whiteboard, + icon: 'iconDraw', + id: "siyuan-jsdraw-pluginwhiteboard", + data: { + fileID: this.editor.getFileID(), + } + } + }); + } + + toDialog() { + const dialog = new Dialog({ + width: "100vw", + height: getFrontend() == "mobile" ? "100vh" : "90vh", + content: `
`, + }); + 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 deleted file mode 100644 index bef75c7..0000000 --- a/src/editorTab.ts +++ /dev/null @@ -1,75 +0,0 @@ -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 new file mode 100644 index 0000000..f7bf264 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,80 @@ +import {showMessage} from "siyuan"; + +export class InternationalizedError extends Error { + readonly key: string; + + constructor(key: string) { + super(key); + this.key = key; + } +} + +export class ErrorReporter { + + static i18n: any; + + constructor(i18n: any) { + ErrorReporter.i18n = i18n; + } + + static error(err: Error, timeout?: number) { + console.error(err); + let errorTxt = err.message; + if(err instanceof InternationalizedError) { + errorTxt = ErrorReporter.i18n[err.key]; + } + if(!timeout) { + timeout = 0; + } + showMessage(errorTxt, timeout, 'error'); + } + +} + +export class SyncIDNotFoundError extends InternationalizedError { + constructor() { + super('errSyncIDNotFound'); + } +} + +export class UnchangedProtyleError extends InternationalizedError { + constructor() { + super('errUnchangedProtyle'); + } +} + +export class MultipleSyncIDsError extends InternationalizedError { + constructor() { + super('errMultipleSyncIDs'); + } +} + +export class GenericSaveError extends InternationalizedError { + constructor() { + super('errSaveGeneric'); + } +} + +export class NotAWhiteboardError extends InternationalizedError { + constructor() { + super('errNotAWhiteboard'); + } +} + +export class InvalidBackgroundColorError extends InternationalizedError { + constructor() { + super('errInvalidBackgroundColor'); + } +} + +export class NoFileIDError extends InternationalizedError { + constructor() { + super('errNoFileID'); + } +} + +export class MustSelectError extends InternationalizedError { + constructor() { + super('errMustSelect'); + } +} \ No newline at end of file diff --git a/src/file.ts b/src/file.ts index 5ce0d20..a3a1a10 100644 --- a/src/file.ts +++ b/src/file.ts @@ -1,37 +1,108 @@ -import {getFileBlob, putFile} from "@/api"; +import {getFileBlob, putFile, removeFile, upload} from "@/api"; +import {ASSETS_PATH, DATA_PATH} from "@/const"; +import {assetPathToIDs, IDsToAssetName} from "@/helper"; -function toFile(title: string, content: string, mimeType: string){ - const blob = new Blob([content], { type: mimeType }); - return new File([blob], title, { type: mimeType }); -} +abstract class PluginFileBase { -export function saveFile(path: string, mimeType: string, content: string) { + protected content: string | null; - const file = toFile(path.split('/').pop(), content, mimeType); + protected fileName: string; + protected folderPath: string; + protected mimeType: string; - try { - putFile(path, false, file); - } catch (error) { - console.error("Error saving file:", error); - throw error; + 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, lastModified: Date.now() }); } } -export async function getFile(path: string) { +export class PluginFile extends PluginFileBase { - 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; + 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; } - }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 18386c7..7041ba5 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,5 +1,5 @@ import { Plugin } from 'siyuan'; -import {DATA_PATH, EMBED_PATH} from "@/const"; +import {ASSETS_PATH} from "@/const"; const drawIcon: string = ` @@ -23,7 +23,7 @@ export function getMenuHTML(icon: string, text: string): string { `; } -export function generateSiyuanId() { +export function generateTimeString() { const now = new Date(); const year = now.getFullYear().toString(); @@ -33,25 +33,84 @@ export function generateSiyuanId() { const minutes = now.getMinutes().toString().padStart(2, '0'); const seconds = now.getSeconds().toString().padStart(2, '0'); - const timestamp = `${year}${month}${day}${hours}${minutes}${seconds}`; + return `${year}${month}${day}${hours}${minutes}${seconds}`; +} + +export function generateRandomString() { 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 idToPath(id: string) { - return DATA_PATH + '/' + id + '.svg'; +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] + }; + } -// [Edit](siyuan://plugins/siyuan-jsdraw-pluginwhiteboard/?icon=iconDraw&title=Drawing&data={"id":"${id}"}) -// ![Drawing](assets/${id}.svg) -export function getPreviewHTML(id: string): string { +export function getMarkdownBlock(fileID: string, syncID: string): string { return ` - + ![Drawing](${IDsToAssetPath(fileID, syncID)}) ` +} + +// 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 e19ecb2..1e0d691 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,44 +1,115 @@ import {Plugin, Protyle} from 'siyuan'; -import {getPreviewHTML, loadIcons, getMenuHTML, generateSiyuanId} from "@/helper"; -import {createEditor, openEditorTab} from "@/editorTab"; +import { + getMarkdownBlock, + loadIcons, + getMenuHTML, + findImgSrc, + imgSrcToIDs, generateTimeString, generateRandomString +} from "@/helper"; +import {EditorManager} from "@/editor"; +import {PluginConfig, PluginConfigViewer} from "@/config"; +import {Analytics} from "@/analytics"; +import {ErrorReporter, MustSelectError, NotAWhiteboardError} from "@/errors"; export default class DrawJSPlugin extends Plugin { - onload() { + config: PluginConfig; + analytics: Analytics; + + async onload() { + + new ErrorReporter(this.i18n); loadIcons(this); - //const id = Math.random().toString(36).substring(7); - this.addTab({ - 'type': "whiteboard", - init() { - createEditor(this); - } - }); + EditorManager.registerTab(this); + + await this.startConfig(); + await this.startAnalytics(); this.protyleSlash = [{ - id: "insert-drawing", - filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"], - html: getMenuHTML("iconDraw", this.i18n.insertDrawing), - callback: (protyle: Protyle) => { - const uid = generateSiyuanId(); - protyle.insert(getPreviewHTML(uid), true, false); - openEditorTab(this, uid); + id: "insert-whiteboard", + filter: ["Insert Drawing", "Add drawing", "Insert whiteboard", "Add whiteboard", "whiteboard", "freehand", "graphics", "jsdraw"], + html: getMenuHTML("iconDraw", this.i18n.insertWhiteboard), + 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); } }]; - } + 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.editWhiteboard, + click: async () => { + void this.analytics.sendEvent('edit'); + (await EditorManager.create(ids.fileID, this)).open(this); + } + }) + }) + + this.addCommand({ + langKey: "editShortcut", + hotkey: "⌥⇧D", + callback: async () => { + this.editSelectedImg().catch(e => ErrorReporter.error(e, 5000)); + }, + }) + + this.addTopBar({ + icon: "iconDraw", + title: this.i18n.editShortcut, + callback: async () => { + await this.editSelectedImg().catch(e => ErrorReporter.error(e, 5000)); + }, + position: "left" + }) - onLayoutReady() { - // This function is automatically called when the layout is loaded. } onunload() { - // This function is automatically called when the plugin is disabled. + void this.analytics.sendEvent("unload"); } uninstall() { - // This function is automatically called when the plugin is uninstalled. + void this.analytics.sendEvent("uninstall"); + } + + private async editSelectedImg() { + + let selectedImg = document.getElementsByClassName('img--select'); + if(selectedImg.length == 0) { + throw new MustSelectError(); + } + + let ids = imgSrcToIDs(findImgSrc(selectedImg[0] as HTMLElement)); + if(ids == null) { + throw new NotAWhiteboardError(); + } + void this.analytics.sendEvent('edit'); + (await EditorManager.create(ids.fileID, this)).open(this); + + } + + 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/protyle.ts b/src/protyle.ts new file mode 100644 index 0000000..6812864 --- /dev/null +++ b/src/protyle.ts @@ -0,0 +1,109 @@ +import {getBlockByID, sql, updateBlock} from "@/api"; +import {assetPathToIDs, IDsToAssetPath} from "@/helper"; +import {MultipleSyncIDsError} from "@/errors"; + +export async function findSyncIDInProtyle(fileID: string, iter?: number): Promise { + + 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 MultipleSyncIDsError(); + } + } + } + + 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 { + 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 = /!\[.*?\]\(([^)\s]+)(?:\s+"[^"]+")?\)/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 0fcc1ad..e2dedbd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "useDefineForClassFields": true, "module": "ESNext", "lib": [ - "ES2020", + "ES2021", "DOM", "DOM.Iterable" ], diff --git a/vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs b/vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs new file mode 100644 index 0000000..f2c6618 --- /dev/null +++ b/vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs @@ -0,0 +1,185 @@ +// 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==