zotero-better-notes/src/modules/template/editorWindow.ts

1038 lines
28 KiB
TypeScript

import YAML = require("yamljs");
import { config, version } from "../../../package.json";
import { showHint } from "../../utils/hint";
import { itemPicker } from "../../utils/itemPicker";
import { getString } from "../../utils/locale";
import { waitUtilAsync } from "../../utils/wait";
export async function showTemplateEditor() {
if (
!addon.data.template.editor.window ||
Components.utils.isDeadWrapper(addon.data.template.editor.window) ||
addon.data.template.editor.window.closed
) {
const windowArgs = {
_initPromise: Zotero.Promise.defer(),
};
const _window = Zotero.getMainWindow().openDialog(
`chrome://${config.addonRef}/content/templateEditor.xhtml`,
`${config.addonRef}-templateEditor`,
`chrome,centerscreen,resizable,status,dialog=no`,
windowArgs,
)!;
addon.data.template.editor.window = _window;
await windowArgs._initPromise.promise;
updateData();
addon.data.template.editor.tableHelper = new ztoolkit.VirtualizedTable(
_window!,
)
.setContainerId("table-container")
.setProp({
id: "templates-table",
// Do not use setLocale, as it modifies the Zotero.Intl.strings
// Set locales directly to columns
columns: [
{
dataKey: "type",
label: "templateEditor-templateType",
width: 60,
fixedWidth: true,
},
{
dataKey: "name",
label: "templateEditor-templateName",
fixedWidth: false,
},
].map((column) =>
Object.assign(column, {
label: getString(column.label),
}),
),
showHeader: true,
multiSelect: false,
staticColumns: true,
disableFontSizeScaling: true,
})
.setProp("getRowCount", () => addon.data.template.editor.templates.length)
.setProp("getRowData", getRowData)
.setProp("onSelectionChange", (selection) => {
updateEditor();
updatePreview();
})
.setProp("onKeyDown", (event: KeyboardEvent) => {
if (
event.key == "Delete" ||
(Zotero.isMac && event.key == "Backspace")
) {
addon.api.template.removeTemplate(getSelectedTemplateName());
refresh(true);
return false;
}
return true;
})
.setProp(
"getRowString",
(index) => addon.data.template.editor.templates[index] || "",
)
.setProp("renderItem", (index, selection, oldElem, columns) => {
let div;
if (oldElem) {
div = oldElem;
div.innerHTML = "";
} else {
div = document.createElement("div");
div.className = "row";
}
div.classList.toggle("selected", selection.isSelected(index));
div.classList.toggle("focused", selection.focused == index);
const rowData = getRowData(index);
for (const column of columns) {
const span = document.createElement("span");
// @ts-ignore
span.className = `cell ${column?.className}`;
const cellData = rowData[column.dataKey as keyof typeof rowData];
span.textContent = cellData;
if (column.dataKey === "type") {
span.style.backgroundColor = getRowLabelColor(cellData);
span.style.borderRadius = "4px";
span.style.paddingInline = "4px";
span.style.marginInline = "2px -2px";
span.style.textAlign = "center";
span.textContent = getString(
"templateEditor-templateDisplayType",
cellData,
);
}
div.append(span);
}
return div;
})
.render();
_window.document
.querySelector("#templateType-help")
?.addEventListener("click", (ev) => {
new addon.data.ztoolkit.Guide().highlight(_window.document, {
title: "About Template Types",
description: ["system", "item", "text"]
.map(
(type) =>
`${getString(
"templateEditor-templateDisplayType",
type,
)}: ${getString("templateEditor-templateHelp", type)}`,
)
.join("\n"),
onNextClick: () => {
Zotero.launchURL(
"https://github.com/windingwind/zotero-better-notes/blob/master/docs/about-note-template.md",
);
},
showButtons: ["next", "close"],
nextBtnText: "Learn more",
closeBtnText: "OK",
position: "center",
});
});
_window.document
.querySelector("#create")
?.addEventListener("click", (ev) => {
createTemplate();
});
_window.document.querySelector("#help")?.addEventListener("click", (ev) => {
Zotero.launchURL(
"https://github.com/windingwind/zotero-better-notes/blob/master/docs/about-note-template.md",
);
});
_window.document.querySelector("#more")?.addEventListener("click", (ev) => {
Zotero.launchURL(
"https://github.com/windingwind/zotero-better-notes/discussions/categories/note-templates",
);
});
_window.document.querySelector("#save")?.addEventListener("click", (ev) => {
saveSelectedTemplate();
});
_window.document
.querySelector("#delete")
?.addEventListener("click", (ev) => {
deleteSelectedTemplate();
});
_window.document
.querySelector("#reset")
?.addEventListener("click", (ev) => {
resetSelectedTemplate();
});
_window.document
.querySelector("#share")
?.addEventListener("click", (ev) => {
shareSelectedTemplate();
});
_window.document
.querySelector("#importClipboard")
?.addEventListener("click", (ev) => {
addon.hooks.onImportTemplateFromClipboard();
});
_window.document
.querySelector("#importNote")
?.addEventListener("click", (ev) => {
importNoteTemplate();
});
_window.document
.querySelector("#backup")
?.addEventListener("click", (ev) => {
backupTemplates();
});
_window.document
.querySelector("#restore")
?.addEventListener("click", (ev) => {
restoreTemplates(_window);
});
_window.document
.querySelector("#editor-type")
?.addEventListener("command", (ev) => {
updateSnippets((ev.target as XULMenuListElement)?.value);
});
// An ugly hack to make the editor refresh exposed
_window.refresh = refresh;
addon.data.template.editor.window?.focus();
const editorWin = (_window.document.querySelector("#editor") as any)
.contentWindow;
await waitUtilAsync(() => editorWin?.loadMonaco);
const isDark = editorWin?.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
const { monaco, editor } = await editorWin.loadMonaco({
language: "javascript",
theme: "vs-" + (isDark ? "dark" : "light"),
});
addon.data.template.editor.monaco = monaco;
addon.data.template.editor.editor = editor;
await initFormats();
}
}
async function refresh(force = false) {
const win = addon.data.template.editor.window;
if (!win) {
return;
}
if (!force && isTemplateNotSaved()) {
const save = win.confirm(getString("alert-templateEditor-unsaved"));
if (save) {
saveSelectedTemplate();
return;
}
}
updateData();
updateTable();
updateEditor();
await updatePreview();
}
function getRowData(index: number) {
const rowData = addon.data.template.editor.templates[index];
if (!rowData) {
return {
name: "",
type: "unknown",
};
}
let templateType = "unknown";
let templateDisplayName = rowData;
if (addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(rowData)) {
templateType = "system";
templateDisplayName = getString(
"templateEditor-templateDisplayName",
// Exclude the first and last character, which are '[' and ']'
rowData.slice(1, -1),
);
} else if (rowData.toLowerCase().startsWith("[item]")) {
templateType = "item";
templateDisplayName = rowData.slice(6);
} else if (rowData.toLowerCase().startsWith("[text]")) {
templateType = "text";
templateDisplayName = rowData.slice(6);
}
return {
name: templateDisplayName,
type: templateType,
};
}
function getRowLabelColor(type: string) {
switch (type) {
case "system":
return "var(--accent-yellow)";
case "item":
return "var(--accent-green)";
case "text":
return "var(--accent-azure)";
default:
return "var(--accent-red)";
}
}
function isTemplateNotSaved() {
const name = getSelectedTemplateName();
if (!name) {
return false;
}
const text = addon.data.template.editor.editor?.getValue() as string;
const savedText = addon.api.template.getTemplateText(name);
if (text !== savedText) {
return true;
}
const { type, name: displayName } = getRowData(getSelectedIndex());
const templateType =
addon.data.template.editor.window?.document.querySelector(
"#editor-type",
) as XULMenuListElement;
const templateName =
addon.data.template.editor.window?.document.querySelector(
"#editor-name",
) as HTMLInputElement;
return type !== templateType.value || displayName !== templateName.value;
}
function updateData() {
addon.data.template.editor.templates = addon.api.template.getTemplateKeys();
}
function updateTable(selectId?: number) {
addon.data.template.editor.tableHelper?.render(selectId);
}
function updateEditor() {
const name = getSelectedTemplateName();
const { type, name: displayName } = getRowData(getSelectedIndex());
const templateText = addon.api.template.getTemplateText(name);
const win = addon.data.template.editor.window;
if (!win) {
return;
}
const templateType = win.document.querySelector(
"#editor-type",
) as XULMenuListElement;
const templateName = win.document.querySelector(
"#editor-name",
) as HTMLInputElement;
const editor = win?.document.getElementById("editor") as HTMLIFrameElement;
const saveTemplate = win?.document.getElementById(
"save",
) as XULButtonElement | null;
const deleteTemplate = win?.document.getElementById(
"delete",
) as XULButtonElement | null;
const resetTemplate = win?.document.getElementById(
"reset",
) as XULButtonElement | null;
const shareTemplate = win?.document.getElementById(
"share",
) as XULButtonElement | null;
const formats = win?.document.getElementById(
"formats-container",
) as HTMLDivElement;
const snippets = win?.document.getElementById(
"snippets-container",
) as HTMLDivElement;
if (!name) {
templateType.value = "unknown";
templateType.setAttribute("disabled", "true");
templateName.value = "";
templateName.setAttribute("disabled", "true");
editor.hidden = true;
saveTemplate?.setAttribute("disabled", "true");
deleteTemplate?.setAttribute("disabled", "true");
deleteTemplate && (deleteTemplate.hidden = false);
shareTemplate?.setAttribute("disabled", "true");
resetTemplate && (resetTemplate.hidden = true);
formats.hidden = true;
snippets.hidden = true;
} else {
templateType.value = type;
templateName.value = displayName;
if (!addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(name)) {
templateType.removeAttribute("disabled");
templateName.removeAttribute("disabled");
deleteTemplate && (deleteTemplate.hidden = false);
resetTemplate && (resetTemplate.hidden = true);
} else {
templateType.setAttribute("disabled", "true");
templateName.setAttribute("disabled", "true");
deleteTemplate?.setAttribute("disabled", "true");
deleteTemplate && (deleteTemplate.hidden = true);
resetTemplate && (resetTemplate.hidden = false);
}
addon.data.template.editor.editor.setValue(templateText);
editor.hidden = false;
saveTemplate?.removeAttribute("disabled");
deleteTemplate?.removeAttribute("disabled");
shareTemplate?.removeAttribute("disabled");
formats.hidden = false;
snippets.hidden = false;
updateSnippets(
(type === "system"
? name.slice(1, -1)
: type) as keyof typeof snippetsStore,
);
}
}
async function initFormats() {
const container =
addon.data.template.editor.window?.document.querySelector(
"#formats-container",
);
if (!container) {
return;
}
container.innerHTML = "";
// Add formats to the container, with each format as a button
for (const format of formatStore) {
const button = document.createElement("div");
button.classList.add("format", format.name);
button.style.backgroundImage = `url("chrome://${config.addonRef}/content/icons/editor/${format.name}.svg")`;
button.dataset.l10nId = `${config.addonRef}-format-${format.name}`;
button.addEventListener("click", () => {
const { editor, monaco } = addon.data.template.editor;
const selection = editor.getSelection();
const range = new monaco.Range(
selection.startLineNumber,
selection.startColumn,
selection.endLineNumber,
selection.endColumn,
);
const textTemplate = format.code;
const source =
editor.getModel().getValueInRange(range) ||
format.defaultText ||
"text";
const text = textTemplate.replace("${text}", source);
editor.executeEdits("", [
{
range,
text,
forceMoveMarkers: true,
},
]);
// Keep the selection after inserting the format
const textBeforeReplace = textTemplate.split("${text}")[0];
const textBeforeLines = textBeforeReplace.split("\n");
const textLines = source.split("\n");
// Calculate the new range
const startLineNumber =
selection.startLineNumber + textBeforeLines.length - 1;
const startColumn =
textBeforeLines.length === 1
? selection.startColumn + textBeforeReplace.length
: textBeforeLines.slice(-1)[0].length + 1;
const endLineNumber = startLineNumber + textLines.length - 1;
const endColumn =
textLines.length === 1
? startColumn + source.length
: textLines.slice(-1)[0].length + 1;
const newRange = new monaco.Range(
startLineNumber,
startColumn,
endLineNumber,
endColumn,
);
editor.setSelection(newRange);
// If editor does not contain a line start with // @use-markdown, insert it
if (
!editor
.getModel()
.getLinesContent()
.some((line: any) => line.startsWith("// @use-markdown"))
) {
editor.executeEdits("", [
{
range: new monaco.Range(1, 1, 1, 1),
text: "// @use-markdown\n",
forceMoveMarkers: true,
},
]);
}
});
container.appendChild(button);
}
}
async function updateSnippets(type: string) {
const container = addon.data.template.editor.window?.document.querySelector(
"#snippets-container",
);
if (!container) {
return;
}
container.innerHTML = "";
const snippets = (
snippetsStore[type as keyof typeof snippetsStore] || []
).concat(snippetsStore.global);
if (!snippets) {
return;
}
// Add snippets to the container, with each snippet as a button
// Dragging the button to the editor will insert the snippet
for (const snippet of snippets) {
const button = document.createElement("span");
button.classList.add("snippet", snippet.type);
button.dataset.l10nId = `${config.addonRef}-snippet-${snippet.name}`;
button.addEventListener("click", () => {
const { editor, monaco } = addon.data.template.editor;
const selection = editor.getSelection();
const range = new monaco.Range(
selection.startLineNumber,
selection.startColumn,
selection.endLineNumber,
selection.endColumn,
);
const text = snippet.code;
editor.executeEdits("", [
{
range,
text,
forceMoveMarkers: true,
},
]);
// Select the inserted text, should compute the new range, as the text can be multi-line
const newRange = new monaco.Range(
selection.startLineNumber,
selection.startColumn,
selection.startLineNumber + text.split("\n").length - 1,
text.split("\n").slice(-1)[0].length + 1,
);
editor.setSelection(newRange);
});
container.appendChild(button);
}
}
async function updatePreview() {
const name = getSelectedTemplateName();
const html = (await addon.api.template.renderTemplatePreview(name))
.replace(/ /g, "#160;")
.replace(/<br>/g, "<br/>")
.replace(/<hr>/g, "<hr/>")
.replace(/<img([^>]+)>/g, "<img$1/>");
const win = addon.data.template.editor.window;
const container = win?.document.getElementById("preview-container");
if (container) {
container.innerHTML = html;
}
}
function getSelectedTemplateName() {
const selectedTemplate =
addon.data.template.editor.templates[getSelectedIndex()];
return selectedTemplate || "";
}
function getSelectedIndex() {
const selectedIndex =
addon.data.template.editor.tableHelper?.treeInstance.selection.selected
.values()
.next().value;
return selectedIndex as number;
}
function createTemplate() {
const template: NoteTemplate = {
name: `New Template: ${new Date().getTime()}`,
text: "",
};
addon.api.template.setTemplate(template);
refresh();
}
async function importNoteTemplate() {
const ids = await itemPicker();
const note: Zotero.Item = Zotero.Items.get(ids).filter((item: Zotero.Item) =>
item.isNote(),
)[0];
if (!note) {
return;
}
const template: NoteTemplate = {
name: `Template from ${note.getNoteTitle()}: ${new Date().getTime()}`,
text: addon.api.sync.getNoteStatus(note.id)?.content || "",
};
addon.api.template.setTemplate(template);
refresh();
}
function saveSelectedTemplate() {
const win = addon.data.template.editor.window;
if (!win) {
return;
}
const templateType = win.document.querySelector(
"#editor-type",
) as XULMenuListElement;
const templateName = win.document.querySelector(
"#editor-name",
) as HTMLInputElement;
const name = getSelectedTemplateName();
const type = templateType.value;
let modifiedName: string;
if (type === "system") {
modifiedName = name;
} else if (type === "unknown") {
modifiedName = templateName.value;
} else {
modifiedName = `[${type}]${templateName.value}`;
}
if (
addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(name) &&
modifiedName !== name
) {
showHint(
`Template ${name} is a system template. Modifying template name is not allowed.`,
);
return;
}
const template = {
name: modifiedName,
text: addon.data.template.editor.editor.getValue() as string,
};
if (
template.text.includes(
"# This template is specifically for importing/sharing",
)
) {
const useImport = addon.data.template.editor.window?.confirm(
getString("alert-templateEditor-shouldImport"),
);
if (useImport) {
addon.hooks.onImportTemplateFromClipboard(template.text);
refresh(true);
return;
}
}
addon.api.template.setTemplate(template);
if (name !== modifiedName) {
addon.api.template.removeTemplate(name);
}
showHint(`Template ${modifiedName} saved.`);
const selectedId =
addon.data.template.editor.tableHelper?.treeInstance.selection.selected
.values()
.next().value;
refresh(true).then(() => updateTable(selectedId));
}
function deleteSelectedTemplate() {
const name = getSelectedTemplateName();
if (addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(name)) {
showHint(
`Template ${name} is a system template. Removing system template is note allowed.`,
);
return;
}
addon.api.template.removeTemplate(name);
refresh(true);
}
function resetSelectedTemplate() {
const name = getSelectedTemplateName();
if (addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(name)) {
addon.data.template.editor.editor.setValue(
addon.api.template.DEFAULT_TEMPLATES.find((t) => t.name === name)?.text ||
"",
);
showHint(`Template ${name} is reset. Please save before leaving.`);
}
}
function shareSelectedTemplate() {
const name = getSelectedTemplateName();
if (!name) {
return;
}
saveSelectedTemplate();
const content = addon.api.template.getTemplateText(name);
const yaml = `# This template is specifically for importing/sharing, using better
# notes 'import from clipboard': copy the content and
# goto Zotero menu bar, click Tools->New Template from Clipboard.
# Do not copy-paste this to better notes template editor directly.
name: "${name}"
zoteroVersion: "${Zotero.version}"
pluginVersion: "${version}"
savedAt: "${new Date().toISOString()}"
content: |-
${content
.split("\n")
.map((line) => ` ${line}`)
.join("\n")}
`;
new ztoolkit.Clipboard().addText(yaml, "text/plain").copy();
showHint(
`Template ${name} is copied to clipboard. To import it, goto Zotero menu->Tools->New Template from Clipboard. `,
);
}
async function backupTemplates() {
const time = new Date().toISOString().replace(/:/g, "-");
const filepath = await new ztoolkit.FilePicker(
"Save backup file",
"save",
[["yaml", "*.yaml"]],
`bn-template-backup-${time}.yaml`,
).open();
if (!filepath) {
return;
}
const keys = addon.api.template.getTemplateKeys();
const templates = keys.map((key) => {
return {
name: key,
text: addon.api.template.getTemplateText(key),
};
});
const yaml = YAML.stringify(templates);
await Zotero.File.putContentsAsync(filepath, yaml);
}
async function restoreTemplates(win: Window) {
const filepath = await new ztoolkit.FilePicker(
"Open backup file",
"open",
[["yaml", "*.yaml"]],
undefined,
win,
"text",
).open();
if (!filepath) {
return;
}
const yaml = (await Zotero.File.getContentsAsync(filepath)) as string;
const templates = YAML.parse(yaml) as NoteTemplate[];
const existingNames = addon.api.template.getTemplateKeys();
for (const t of templates) {
if (existingNames.includes(t.name)) {
const overwrite = win.confirm(
`Template ${t.name} already exists. Overwrite?`,
);
if (!overwrite) {
continue;
}
}
addon.api.template.setTemplate(t);
}
await refresh(true);
}
const formatStore = [
{
name: "bold",
code: "**${text}**",
},
{
name: "italic",
code: "_${text}_",
},
{
name: "strikethrough",
code: "~~${text}~~",
},
{
name: "underline",
code: "<u>${text}</u>",
},
{
name: "superscript",
code: "<sup>${text}</sup>",
},
{ name: "subscript", code: "<sub>${text}</sub>" },
{
name: "textColor",
code: '<span style="color: orange">${text}</span>',
},
{
name: "link",
code: "[${text}](url)",
},
{
name: "quote",
code: "\n> ${text}",
},
{
name: "monospaced",
code: "<code>${text}</code>",
},
{
name: "code",
code: "\n<pre>\n${text}\n</pre>\n",
},
{
name: "table",
code: "\n| ${text} | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |\n",
},
{
name: "h1",
code: "\n# ${text}",
},
{
name: "h2",
code: "\n## ${text}",
},
{
name: "h3",
code: "\n### ${text}",
},
{
name: "bullet",
code: "\n- ${text}",
},
{
name: "numbered",
code: "\n1. ${text}",
},
{
name: "inlineMath",
code: "$${text}$",
defaultText: "e=mc^2",
},
{
name: "blockMath",
code: "\n$$\n${text}\n$$\n",
defaultText: "e=mc^2",
},
{
name: "inlineScript",
code: "${ ${text} }",
defaultText: "Zotero.version",
},
{
name: "blockScript",
code: "\n${{\n ${text}\n}}$\n",
defaultText: "return Zotero.version;",
},
];
const snippetsStore = {
global: [
{
name: "useMarkdown",
code: "\n// @use-markdown\n",
type: "syntax",
},
{
name: "useRefresh",
code: "\n// @use-refresh\n",
type: "syntax",
},
{
name: "dryRunFlag",
code: "_env.dryRun",
type: "variable",
},
],
item: [
{
name: "itemBeforeLoop",
code: "\n// @beforeloop-begin\n\n// @beforeloop-end\n",
type: "syntax",
},
{
name: "itemInLoop",
code: "\n// @default-begin\n\n// @default-end\n",
type: "syntax",
},
{
name: "itemAfterLoop",
code: "\n// @afterloop-begin\n\n// @afterloop-end\n",
type: "syntax",
},
{
name: "itemItems",
code: "items",
type: "variable",
},
{
name: "itemItem",
code: "item",
type: "variable",
},
{
name: "itemTopItem",
code: "topItem",
type: "variable",
},
{
name: "itemTargetNoteItem",
code: "targetNoteItem",
type: "variable",
},
{
name: "itemCopyNoteImage",
code: "${copyNoteImage(...)}",
type: "expression",
},
{
name: "itemSharedObj",
code: "sharedObj",
type: "variable",
},
{
name: "itemFieldTitle",
code: '${topItem.getField("title")}',
type: "expression",
},
{
name: "itemFieldAbstract",
code: '${topItem.getField("abstractNote")}',
type: "expression",
},
{
name: "itemFieldCitKey",
code: '${topItem.getField("citationKey")}',
type: "expression",
},
{
name: "itemFieldDate",
code: '${topItem.getField("date")}',
type: "expression",
},
{
name: "itemFieldDOI",
code: '${topItem.getField("DOI")}',
type: "expression",
},
{
name: "itemFieldDOIURL",
code: `
\${{
const doi = topItem.getField("DOI");
const url = topItem.getField("url");
if (doi) {
return \`DOI: <a href="https://doi.org/\${doi}">\${doi}</a>\`;
} else {
return \`URL: <a href="\${url}">\${url}</a>\`;
}
}}$
`,
type: "expression",
},
{
name: "itemFieldAuthors",
code: '${topItem.getCreators().map((v)=>v.firstName+" "+v.lastName).join("; ")}',
type: "expression",
},
{
name: "itemFieldJournal",
code: '${topItem.getField("publicationTitle")}',
type: "expression",
},
{
name: "itemFieldTitleTranslation",
code: '${topItem.getField("titleTranslation")}',
type: "expression",
},
],
text: [
{
name: "textTargetNoteItem",
code: "targetNoteItem",
type: "variable",
},
{
name: "textSharedObj",
code: "sharedObj",
type: "variable",
},
],
QuickInsertV2: [
{
name: "quickInsertLink",
code: "link",
type: "variable",
},
{
name: "quickInsertLinkText",
code: "linkText",
type: "variable",
},
{
name: "quickInsertSubNoteItem",
code: "subNoteItem",
type: "variable",
},
{
name: "quickInsertNoteItem",
code: "noteItem",
type: "variable",
},
],
QuickImportV2: [
{
name: "quickImportLink",
code: "link",
type: "variable",
},
{
name: "quickImportNoteItem",
code: "noteItem",
type: "variable",
},
],
QuickNoteV5: [
{
name: "quickNoteAnnotationItem",
code: "annotationItem",
type: "variable",
},
{
name: "quickNoteTopItem",
code: "topItem",
type: "variable",
},
{
name: "quickNoteNoteItem",
code: "noteItem",
type: "variable",
},
],
ExportMDFileNameV2: [
{
name: "exportMDFileNameNoteItem",
code: "noteItem",
type: "variable",
},
],
ExportMDFileHeaderV2: [
{
name: "exportMDFileHeaderNoteItem",
code: "noteItem",
type: "variable",
},
],
ExportMDFileContent: [
{
name: "exportMDFileContentNoteItem",
code: "noteItem",
type: "variable",
},
{
name: "exportMDFileContentMDContent",
code: "mdContent",
type: "variable",
},
],
};