From 8463a27ca83603ef496855bec37bb8e34e5b1ef7 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Sat, 20 Jul 2024 17:31:19 +0800 Subject: [PATCH] add: update content from template --- addon/locale/en-US/addon.ftl | 1 + addon/locale/it-IT/addon.ftl | 1 + addon/locale/ru-RU/addon.ftl | 1 + addon/locale/tr-TR/addon.ftl | 1 + addon/locale/zh-CN/addon.ftl | 1 + docs/about-note-template.md | 8 +++ src/api.ts | 2 + src/elements/workspace/workspace.ts | 7 ++- src/hooks.ts | 4 ++ src/modules/editor/toolbar.ts | 8 +++ src/modules/template/api.ts | 31 +++++++++++- src/modules/template/refresh.ts | 78 +++++++++++++++++++++++++++++ src/utils/editor.ts | 5 ++ src/utils/str.ts | 13 +++-- 14 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 src/modules/template/refresh.ts diff --git a/addon/locale/en-US/addon.ftl b/addon/locale/en-US/addon.ftl index 3b8e05b..33f00c3 100644 --- a/addon/locale/en-US/addon.ftl +++ b/addon/locale/en-US/addon.ftl @@ -65,6 +65,7 @@ editor-toolbar-settings-openAsTab = Open as tab editor-toolbar-settings-openAsWindow = Open as window editor-toolbar-settings-showInLibrary = Show in Library editor-toolbar-settings-insertTemplate = Insert template +editor-toolbar-settings-refreshTemplates = Update content from templates (Beta) editor-toolbar-settings-copyLink = Copy link (L{ $line }) editor-toolbar-settings-copyLinkAtSection = Copy link (Sec. { $section }) editor-toolbar-settings-openParent = Open Attachment diff --git a/addon/locale/it-IT/addon.ftl b/addon/locale/it-IT/addon.ftl index e67809e..8ea0c3d 100644 --- a/addon/locale/it-IT/addon.ftl +++ b/addon/locale/it-IT/addon.ftl @@ -61,6 +61,7 @@ editor-toolbar-settings-openAsTab = Open as tab editor-toolbar-settings-openAsWindow = Open as window editor-toolbar-settings-showInLibrary = Show in Library editor-toolbar-settings-insertTemplate = Inserisci template +editor-toolbar-settings-refreshTemplates = Update content from templates (Beta) editor-toolbar-settings-copyLink = Copia link (L{ $line }) editor-toolbar-settings-copyLinkAtSection = Copia link (Sec. { $section }) editor-toolbar-settings-openParent = Apri allegato diff --git a/addon/locale/ru-RU/addon.ftl b/addon/locale/ru-RU/addon.ftl index 654318b..bbaefa0 100644 --- a/addon/locale/ru-RU/addon.ftl +++ b/addon/locale/ru-RU/addon.ftl @@ -65,6 +65,7 @@ editor-toolbar-settings-openAsTab = Open as tab editor-toolbar-settings-openAsWindow = Open as window editor-toolbar-settings-showInLibrary = Show in Library editor-toolbar-settings-insertTemplate=Вставить шаблон +editor-toolbar-settings-refreshTemplates = Update content from templates (Beta) editor-toolbar-settings-copyLink = Копировать Ссылку (L{ $line }) editor-toolbar-settings-copyLinkAtSection = Копировать Ссылку (Sec. { $section }) editor-toolbar-settings-openParent=Открыть вложение diff --git a/addon/locale/tr-TR/addon.ftl b/addon/locale/tr-TR/addon.ftl index 2db6aed..826d747 100644 --- a/addon/locale/tr-TR/addon.ftl +++ b/addon/locale/tr-TR/addon.ftl @@ -65,6 +65,7 @@ editor-toolbar-settings-openAsTab = Open as tab editor-toolbar-settings-openAsWindow = Open as window editor-toolbar-settings-showInLibrary = Show in Library editor-toolbar-settings-insertTemplate = Insert template +editor-toolbar-settings-refreshTemplates = Update content from templates (Beta) editor-toolbar-settings-copyLink = Copy link (L{ $line }) editor-toolbar-settings-copyLinkAtSection =Copy link (Sec. { $section }) editor-toolbar-settings-openParent = Eki Aç diff --git a/addon/locale/zh-CN/addon.ftl b/addon/locale/zh-CN/addon.ftl index 847c9e4..e2516ce 100644 --- a/addon/locale/zh-CN/addon.ftl +++ b/addon/locale/zh-CN/addon.ftl @@ -65,6 +65,7 @@ editor-toolbar-settings-openAsTab = 在标签页中打开 editor-toolbar-settings-openAsWindow = 在窗口中打开 editor-toolbar-settings-showInLibrary = 在文库中显示 editor-toolbar-settings-insertTemplate=插入模板 +editor-toolbar-settings-refreshTemplates = 更新模板生成内容 (Beta) editor-toolbar-settings-copyLink=复制行(L{ $line }) editor-toolbar-settings-copyLinkAtSection=复制节(Sec. { $section }) editor-toolbar-settings-openParent=打开附件 diff --git a/docs/about-note-template.md b/docs/about-note-template.md index c66179c..923f96b 100644 --- a/docs/about-note-template.md +++ b/docs/about-note-template.md @@ -83,6 +83,14 @@ Pragmas are lines start with `// @`. They have special effect and will not be re Let the compiler know you are using markdown. Otherwise the template will be processed as HTML. +### `// @use-update` + +Allow the generated content to be updated using the `Update content from templates` in the note editor. +The generated content will be wrapped in separators with a YAML metadata section for update. +This is a beta feature and can be changed/removed in the future. + +> The template with this pragma should not contain any separator (`---` or `
`) in the content. + ### `// @author` Mark the code belongs to you. Your GitHub account or your email. diff --git a/src/api.ts b/src/api.ts index 09ecf5f..28d86e4 100644 --- a/src/api.ts +++ b/src/api.ts @@ -63,6 +63,7 @@ import { replace, moveHeading, updateHeadingTextAtLine, + getLineCount, } from "./utils/editor"; import { addLineToNote, @@ -153,6 +154,7 @@ const editor = { getLineAtCursor, getSectionAtCursor, getPositionAtLine, + getLineCount, getTextBetween, getTextBetweenLines, moveHeading, diff --git a/src/elements/workspace/workspace.ts b/src/elements/workspace/workspace.ts index 17025d4..0251436 100644 --- a/src/elements/workspace/workspace.ts +++ b/src/elements/workspace/workspace.ts @@ -1,5 +1,10 @@ import { config } from "../../../package.json"; -import { getPrefJSON, registerPrefObserver, setPref, unregisterPrefObserver } from "../../utils/prefs"; +import { + getPrefJSON, + registerPrefObserver, + setPref, + unregisterPrefObserver, +} from "../../utils/prefs"; import { waitUtilAsync } from "../../utils/wait"; import { PluginCEBase } from "../base"; import { ContextPane } from "./contextPane"; diff --git a/src/hooks.ts b/src/hooks.ts index f6d52f5..22dc7a9 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -41,6 +41,7 @@ import { getPref, setPref } from "./utils/prefs"; import { closeRelationWorker } from "./utils/relation"; import { registerNoteLinkSection } from "./modules/workspace/link"; import { showUserGuide } from "./modules/userGuide"; +import { refreshTemplatesInNote } from "./modules/template/refresh"; async function onStartup() { await Promise.all([ @@ -234,6 +235,8 @@ const onUpdateTemplatePicker = updateTemplatePicker; const onImportTemplateFromClipboard = importTemplateFromClipboard; +const onRefreshTemplatesInNote = refreshTemplatesInNote; + const onShowImageViewer = showImageViewer; const onShowExportNoteOptions = showExportNoteOptions; @@ -269,6 +272,7 @@ export default { onShowTemplatePicker, onUpdateTemplatePicker, onImportTemplateFromClipboard, + onRefreshTemplatesInNote, onShowImageViewer, onShowExportNoteOptions, onShowSyncDiff, diff --git a/src/modules/editor/toolbar.ts b/src/modules/editor/toolbar.ts index c7cc347..af9b4e8 100644 --- a/src/modules/editor/toolbar.ts +++ b/src/modules/editor/toolbar.ts @@ -6,6 +6,7 @@ import { getNoteLink } from "../../utils/link"; import { getString } from "../../utils/locale"; import { openLinkCreator } from "../../utils/linkCreator"; import { slice } from "../../utils/str"; +import { refreshTemplatesInNote } from "../template/refresh"; export async function initEditorToolbar(editor: Zotero.EditorInstance) { const noteItem = editor._item; @@ -122,6 +123,13 @@ async function getMenuData(editor: Zotero.EditorInstance) { }); }, }, + { + id: makeId("settings-refreshTemplates"), + text: getString("editor.toolbar.settings.refreshTemplates"), + callback: (e) => { + addon.hooks.onRefreshTemplatesInNote(e.editor); + }, + }, { type: "splitter", }, diff --git a/src/modules/template/api.ts b/src/modules/template/api.ts index 6f16513..4651572 100644 --- a/src/modules/template/api.ts +++ b/src/modules/template/api.ts @@ -1,3 +1,4 @@ +import YAML = require("yamljs"); import { itemPicker } from "../../utils/itemPicker"; import { getString } from "../../utils/locale"; import { fill, slice } from "../../utils/str"; @@ -114,7 +115,7 @@ async function runTextTemplate( const { targetNoteId, dryRun } = options; const targetNoteItem = Zotero.Items.get(targetNoteId || -1); const sharedObj = {}; - return await runTemplate( + let renderedString = await runTemplate( key, "targetNoteItem, sharedObj", [targetNoteItem, sharedObj], @@ -122,6 +123,15 @@ async function runTextTemplate( dryRun, }, ); + + const templateText = addon.api.template.getTemplateText(key); + // Find if any line starts with // @use-refresh using regex + if (/\/\/ @use-refresh/.test(templateText)) { + renderedString = wrapYAMLData(renderedString, { + template: key, + }); + } + return renderedString; } async function runItemTemplate( @@ -208,10 +218,20 @@ async function runItemTemplate( ); const html = results.join("\n"); - return await addon.api.convert.note2html(copyImageRefNotes, { + let renderedString = await addon.api.convert.note2html(copyImageRefNotes, { targetNoteItem, html, }); + + const templateText = addon.api.template.getTemplateText(key); + // Find if any line starts with // @use-refresh using regex + if (/\/\/ @use-refresh/.test(templateText)) { + renderedString = wrapYAMLData(renderedString, { + template: key, + items: Array.from(items.map((item) => item.libraryKey)), + }); + } + return renderedString; } async function getItemTemplateData() { @@ -259,3 +279,10 @@ async function getItemTemplateData() { } return await itemPicker(); } + +function wrapYAMLData(str: string, data: any) { + const yamlContent = YAML.stringify(data, 4); + return `
+
${yamlContent}
${str} +
`; +} diff --git a/src/modules/template/refresh.ts b/src/modules/template/refresh.ts new file mode 100644 index 0000000..ca7484e --- /dev/null +++ b/src/modules/template/refresh.ts @@ -0,0 +1,78 @@ +import YAML = require("yamljs"); +import { htmlUnescape } from "../../utils/str"; + +export { refreshTemplatesInNote }; + +async function refreshTemplatesInNote(editor: Zotero.EditorInstance) { + const lines = addon.api.note.getLinesInNote(editor._item); + let startIndex = -1; + const matchedIndexPairs: { from: number; to: number }[] = []; + + function isTemplateWrapperStart(index: number) { + return ( + index < lines.length - 1 && + lines[index].trim() === "
" && + lines[index + 1].trim().startsWith("
") &&
+      lines[index + 1].includes("template: ")
+    );
+  }
+
+  function isTemplateWrapperEnd(index: number) {
+    return startIndex >= 0 && lines[index].trim() === "
"; + } + + for (let i = 0; i < lines.length; i++) { + // Match: 1. current line is
; 2. next line is
 and contains template key; 3. then contains any number of lines; until end with 
line + if (isTemplateWrapperStart(i)) { + startIndex = i; + continue; + } + if (isTemplateWrapperEnd(i)) { + matchedIndexPairs.push({ from: startIndex, to: i }); + startIndex = -1; + } + } + + let indexOffset = 0; + for (const { from, to } of matchedIndexPairs) { + const yamlContent = htmlUnescape( + lines[from + 1].replace("
", "").replace("
", ""), + { excludeLineBreak: true }, + ); + const { template, items } = YAML.parse(yamlContent) as { + template: string; + items?: string[]; + }; + let html = ""; + if (template.toLowerCase().startsWith("[item]")) { + html = await addon.api.template.runItemTemplate(template, { + targetNoteId: editor._item.id, + itemIds: items + ?.map((id) => { + const [libraryID, key] = id.split("/"); + return Zotero.Items.getIDFromLibraryAndKey(Number(libraryID), key); + }) + .filter((id) => !!id) as number[], + }); + } else { + html = await addon.api.template.runTextTemplate(template, { + targetNoteId: editor._item.id, + }); + } + const currentLineCount = addon.api.editor.getLineCount(editor); + addon.api.editor.del( + editor, + addon.api.editor.getPositionAtLine(editor, from + indexOffset, "start"), + addon.api.editor.getPositionAtLine(editor, to + indexOffset + 1, "start"), + ); + const position = addon.api.editor.getPositionAtLine( + editor, + from + indexOffset, + "start", + ); + addon.api.editor.insert(editor, html, position); + + const newLineCount = addon.api.editor.getLineCount(editor); + indexOffset -= currentLineCount - newLineCount; + } +} diff --git a/src/utils/editor.ts b/src/utils/editor.ts index 3334ed6..0b60c71 100644 --- a/src/utils/editor.ts +++ b/src/utils/editor.ts @@ -19,6 +19,7 @@ export { getSectionAtCursor, getPositionAtLine, getPositionAtCursor, + getLineCount, getURLAtCursor, updateImageDimensionsAtCursor, updateURLAtCursor, @@ -251,6 +252,10 @@ function getPositionAtLine( ); } +function getLineCount(editor: Zotero.EditorInstance) { + return getEditorCore(editor).view.docView.children.length; +} + function getURLAtCursor(editor: Zotero.EditorInstance) { const core = getEditorCore(editor); return core.pluginState.link.getHref(core.view.state); diff --git a/src/utils/str.ts b/src/utils/str.ts index a356ba5..eb2750e 100644 --- a/src/utils/str.ts +++ b/src/utils/str.ts @@ -127,13 +127,20 @@ export function htmlEscape(doc: Document, str: string) { return div.innerHTML.replace(/"/g, """).replace(/'/g, "'"); } -export function htmlUnescape(str: string) { - const map = { +export function htmlUnescape( + str: string, + options: { + excludeLineBreak?: boolean; + } = {}, +) { + const map: Record = { " ": " ", """: '"', "'": "'", - "\n": "", }; + if (!options.excludeLineBreak) { + map["\n"] = ""; + } const re = new RegExp(Object.keys(map).join("|"), "g"); return str.replace(re, function (match) { return map[match as keyof typeof map];