add: update content from template

This commit is contained in:
windingwind 2024-07-20 17:31:19 +08:00
parent f211b62959
commit 8463a27ca8
14 changed files with 155 additions and 6 deletions

View File

@ -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

View File

@ -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

View File

@ -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=Открыть вложение

View File

@ -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ç

View File

@ -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=打开附件

View File

@ -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 `<hr>`) in the content.
### `// @author`
Mark the code belongs to you. Your GitHub account or your email.

View File

@ -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,

View File

@ -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";

View File

@ -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,

View File

@ -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",
},

View File

@ -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 `<hr>
<pre>${yamlContent}</pre>${str}
<hr>`;
}

View File

@ -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() === "<hr>" &&
lines[index + 1].trim().startsWith("<pre>") &&
lines[index + 1].includes("template: ")
);
}
function isTemplateWrapperEnd(index: number) {
return startIndex >= 0 && lines[index].trim() === "<hr>";
}
for (let i = 0; i < lines.length; i++) {
// Match: 1. current line is <hr>; 2. next line is <pre> and contains template key; 3. then contains any number of lines; until end with <hr> 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("<pre>", "").replace("</pre>", ""),
{ 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;
}
}

View File

@ -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);

View File

@ -127,13 +127,20 @@ export function htmlEscape(doc: Document, str: string) {
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
export function htmlUnescape(str: string) {
const map = {
export function htmlUnescape(
str: string,
options: {
excludeLineBreak?: boolean;
} = {},
) {
const map: Record<string, string> = {
"&nbsp;": " ",
"&quot;": '"',
"&#39;": "'",
"\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];