Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/ui/bibliography-renderer/Message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const FORMAT_REFERENCES: string = "format_references";
export const GET_REFERENCE_BY_ID: string = "get_reference_by_id";
51 changes: 51 additions & 0 deletions src/ui/bibliography-renderer/extract-references.ts
Original file line number Diff line number Diff line change
@@ -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;
}
85 changes: 85 additions & 0 deletions src/ui/bibliography-renderer/generateScript.ts
Original file line number Diff line number Diff line change
@@ -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;
}
60 changes: 49 additions & 11 deletions src/ui/bibliography-renderer/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,21 +20,57 @@ export async function registerBibliographyRenderer(): Promise<void> {
"./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;
Copy link
Copy Markdown
Member

@laurent22 laurent22 Aug 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That shouldn't be "any" especially since you have defined a type for the references.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see

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);
Expand Down
5 changes: 5 additions & 0 deletions src/ui/bibliography-renderer/reference-patterns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const patterns: RegExp[] = [
/\[-@(\w|:|\?|\-)+\]/g,
/\[@(\w|:|\?|\-)+\]/g,
/@(\w|:|\?|\-)+/g,
];
98 changes: 54 additions & 44 deletions src/ui/bibliography-renderer/render-list-content-script.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,47 @@
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 {
plugin: function (markdownIt, _options) {
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");

Expand All @@ -70,9 +55,34 @@ export default function (context) {
return `
<h1 id="references_title" style="display:none">References</h1>
<div id="references_list"></div>
<style onload='${script.replace(/\n/g, " ")}'/>
<style onload='${script.replace(/\n/g, " ")}' />
`;
}
};

let inlineReferenceCounter: number = 0;
markdownIt.renderer.rules["inline_reference"] = function (
tokens,
idx,
options
) {
const token = tokens[idx];
let content = token.content;
for (let i = 0; i < patterns.length; i++) {
const pattern: RegExp = patterns[i];

const matches: string[] = content.match(pattern);
if (matches && matches.length) {
for (let j = 0; j < matches.length; j++) {
const match = matches[j];
const viewId = `bibtex_reference_${inlineReferenceCounter}`;
const html = `<span id="${viewId}">(Loading...)</span>`;
content = content.replace(match, html);
inlineReferenceCounter++;
}
}
}
return content;
};
},
};
}