diff --git a/src/ui/bibliography-renderer/Message.ts b/src/ui/bibliography-renderer/Message.ts new file mode 100644 index 0000000..8f52ba8 --- /dev/null +++ b/src/ui/bibliography-renderer/Message.ts @@ -0,0 +1,2 @@ +export const FORMAT_REFERENCES: string = "format_references"; +export const GET_REFERENCE_BY_ID: string = "get_reference_by_id"; diff --git a/src/ui/bibliography-renderer/extract-references.ts b/src/ui/bibliography-renderer/extract-references.ts new file mode 100644 index 0000000..a7931b3 --- /dev/null +++ b/src/ui/bibliography-renderer/extract-references.ts @@ -0,0 +1,51 @@ +/** + * Given a list of markdown tokens and their children, + * Replaces every text token that contains a reference with an "inline_reference" token + * and returns a list of reference IDs that exists in the markdown tree + * Uses recursive Depth-First-Search + */ +export function extractReferences(tokens: any[], Token: any): string[] { + const ids: string[] = []; + const referencePattern = /@(\w|:|\?|\-)+/g; + + /* Collect all words that matches the regular expression */ + DFS(tokens, Token); + + function DFS(nodes: any[], Token: any): void { + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].type === "text") { + // If it's the default format, keep it as it is + // Example: [@dijkstraNoteTwoProblems1959](https://scholar.google.com/scholar?hl=en&as_sdt=0%2C5&q=two+problems+connexion+with+graphs&btnG=&oq=note+on+two+problems+in+connexion+) + if ( + nodes[i].content.startsWith("@") && + i - 1 >= 0 && + nodes[i - 1].type === "link_open" && + i + 1 < nodes.length && + nodes[i + 1].type === "link_close" + ) { + ids.push(nodes[i].content.substring(1)); + continue; + } + + const matches: string[] = + nodes[i].content.match(referencePattern); + if (!matches || !matches.length) continue; + + // Replace the current textToken with a reference Token with the same content + // Just to be parsed later on by the renderer + const content = nodes[i].content; + nodes[i] = new Token("inline_reference", "", 0); + nodes[i].content = content; + + // push all the matches to the ids array, without "@" + matches.forEach((match) => ids.push(match.substring(1))); + } + // Recursively search child nodes + else if (nodes[i].children && nodes[i].children.length) { + DFS(nodes[i].children, Token); + } + } + } + + return ids; +} diff --git a/src/ui/bibliography-renderer/generateScript.ts b/src/ui/bibliography-renderer/generateScript.ts new file mode 100644 index 0000000..321486c --- /dev/null +++ b/src/ui/bibliography-renderer/generateScript.ts @@ -0,0 +1,85 @@ +import { GET_REFERENCE_BY_ID } from "./Message"; +import { patterns } from "./reference-patterns"; + +/** + * Given a list of tokens and their children, + * return a string of javascript code that requests the reference data + */ +export function generateScript(tokens: any[], contentScriptId: string): string { + let js: string = ""; + + /* Collect all words that matches the regular expression */ + let inlineReferenceCounter: number = 0; + DFS(tokens, contentScriptId); + + function DFS(nodes: any[], contentScriptId: string): void { + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].type === "inline_reference") { + let matches: string[]; + + matches = nodes[i].content.match(patterns[0]); + if (matches && matches.length) { + for (let j = 0; j < matches.length; j++) { + const match: string = matches[j]; + const refId: string = match.substring( + 3, + match.length - 1 + ); + const viewId = `bibtex_reference_${inlineReferenceCounter}`; + const script: string = ` + webviewApi.postMessage("${contentScriptId}", ${JSON.stringify( + { + type: GET_REFERENCE_BY_ID, + id: refId, + } + )}).then(ref => { + if (ref && ref.year) { + const view = document.getElementById("${viewId}"); + view.textContent = "(" + ref.year + ")"; + } + }); + `; + + js += script; + inlineReferenceCounter++; + } + } + + matches = nodes[i].content.match(patterns[1]); + if (matches && matches.length) { + for (let j = 0; j < matches.length; j++) { + const match: string = matches[j]; + const refId: string = match.substring( + 2, + match.length - 1 + ); + const viewId = `bibtex_reference_${inlineReferenceCounter}`; + const script: string = ` + webviewApi.postMessage("${contentScriptId}", ${JSON.stringify( + { + type: GET_REFERENCE_BY_ID, + id: refId, + } + )}).then(ref => { + if (ref && ref.year && ref.auth) { + const view = document.getElementById("${viewId}"); + view.textContent = "(" + ref.auth + " " + ref.year + ")"; + } + }); + `; + + js += script; + inlineReferenceCounter++; + } + } + } + + // Recursively search child nodes + else if (nodes[i].children && nodes[i].children.length) { + DFS(nodes[i].children, contentScriptId); + } + } + } + + return js; +} diff --git a/src/ui/bibliography-renderer/index.ts b/src/ui/bibliography-renderer/index.ts index 619d0c6..2a23cc4 100644 --- a/src/ui/bibliography-renderer/index.ts +++ b/src/ui/bibliography-renderer/index.ts @@ -1,11 +1,13 @@ import joplin from "api"; import { ContentScriptType } from "api/types"; +import { getDateYear } from "../../util/get-date.util"; +import { DataStore } from "../../data/data-store"; import { CSLProcessor } from "../../util/csl-processor"; import { REFERENCE_LIST_CONTENT_SCRIPT_ID, SETTINGS_CSL_FILE_PATH_ID, - MESSAGE_RESTART_APP, } from "../../constants"; +import { FORMAT_REFERENCES, GET_REFERENCE_BY_ID } from "./Message"; /** * Render the full list of references at the end of the note viewer @@ -18,21 +20,57 @@ export async function registerBibliographyRenderer(): Promise { "./ui/bibliography-renderer/render-list-content-script.js" ); - /** - * Format the references according to the style specified by the user - */ const processor = CSLProcessor.getInstance(); + + /* Handle messages sent by the content script */ await joplin.contentScripts.onMessage( REFERENCE_LIST_CONTENT_SCRIPT_ID, - (IDs: string[]) => { - IDs = [...new Set(IDs)]; // Filter duplicate references + (req: { type: string }) => { + switch (req.type) { + /** + * Format the references according to the style specified by the user + */ + case FORMAT_REFERENCES: + /** + * Filter duplicate IDs + * Filter fake IDs (IDs that don't correspond to actual reference objects) + */ + let IDs: string[] = req["IDs"]; + IDs = [...new Set(IDs)].filter((id) => { + try { + DataStore.getReferenceById(id); + return true; + } catch (e) { + return false; + } + }); + + /** + * Apply the specified citation style to the references + * Does html-encoding by default + */ + return processor.formatRefs(IDs); + break; - /** - * Apply the specified citation style to the references - * Note: Does html-encoding by default - */ - return processor.formatRefs(IDs); + case GET_REFERENCE_BY_ID: + console.log(req); + const id = req["id"]; + let ans: any; + try { + ans = DataStore.getReferenceById(id); + } catch (e) { + console.log(e); + return null; + } + ans = { + ...ans, + auth: ans["author"][0]["given"], + year: getDateYear(ans), + }; + return ans; + break; + } } ); setProcessorStyle(processor); diff --git a/src/ui/bibliography-renderer/reference-patterns.ts b/src/ui/bibliography-renderer/reference-patterns.ts new file mode 100644 index 0000000..ab69ab9 --- /dev/null +++ b/src/ui/bibliography-renderer/reference-patterns.ts @@ -0,0 +1,5 @@ +export const patterns: RegExp[] = [ + /\[-@(\w|:|\?|\-)+\]/g, + /\[@(\w|:|\?|\-)+\]/g, + /@(\w|:|\?|\-)+/g, +]; diff --git a/src/ui/bibliography-renderer/render-list-content-script.ts b/src/ui/bibliography-renderer/render-list-content-script.ts index e9ffadb..9e1f975 100644 --- a/src/ui/bibliography-renderer/render-list-content-script.ts +++ b/src/ui/bibliography-renderer/render-list-content-script.ts @@ -1,4 +1,7 @@ -import { Reference } from "../../model/reference.model"; +import { extractReferences } from "./extract-references"; +import { generateScript } from "./generateScript"; +import { FORMAT_REFERENCES } from "./Message"; +import { patterns } from "./reference-patterns"; export default function (context) { return { @@ -6,57 +9,39 @@ export default function (context) { const contentScriptId = context.contentScriptId; /* Appends a new custom token for references list */ - markdownIt.core.ruler.push("reference_list", async (state) => { - /* Collect references from the note body using Depth-first-search */ - const ids: Reference[] = []; - dfs(state.tokens); - - function dfs(children: any[]): void { - if (!children) return; - - /* Search for three consecutive tokens: "link_open", "text", and "link_close" */ - for (let i = 1; i < children.length - 1; i++) { - const curr = children[i], - prev = children[i - 1], - next = children[i + 1]; - if ( - prev["type"] === "link_open" && - curr["type"] === "text" && - next["type"] === "link_close" && - curr.content && - curr.content.length > 1 && - curr.content.startsWith("@") - ) { - const id = curr.content.substring(1); - ids.push(id); - } else { - if (curr["children"]) dfs(curr["children"]); - } - } - // first and last child that were not traversed previously - const last = children[children.length - 1], - first = children[0]; - if (last["children"]) dfs(last["children"]); - if (first["children"]) dfs(first["children"]); - } + markdownIt.core.ruler.push("reference_list", (state) => { + /* Collect references from the note body */ + const ids: string[] = extractReferences( + state.tokens, + state.Token + ); + const script: string = generateScript( + state.tokens, + contentScriptId + ); /* Append reference_list token */ let token = new state.Token("reference_list", "", 0); token.attrSet("refs", ids); + token.attrSet("script", script); state.tokens.push(token); }); - /* Define how to render the previously defined token */ - markdownIt.renderer.rules["reference_list"] = renderReferenceList; - - function renderReferenceList(tokens, idx, options) { + /* Define how to render the reference_list token */ + markdownIt.renderer.rules["reference_list"] = function ( + tokens, + idx, + options + ) { let IDs: string[] = tokens[idx]["attrs"][0][1]; if (IDs.length === 0) return ""; + let script: string = tokens[idx]["attrs"][1][1]; - const script: string = ` - webviewApi.postMessage("${contentScriptId}", ${JSON.stringify( - IDs - )}).then(html => { + script += ` + webviewApi.postMessage("${contentScriptId}", ${JSON.stringify({ + type: FORMAT_REFERENCES, + IDs, + })}).then(html => { const referenceListView = document.getElementById("references_list"); const referenceTitleView = document.getElementById("references_title"); @@ -70,9 +55,34 @@ export default function (context) { return `

References

-