Skip to content

Commit

Permalink
[#54081] Enable customizable MyST roles
Browse files Browse the repository at this point in the history
  • Loading branch information
MaciejWas authored and AdamOlech committed Jan 19, 2024
1 parent ca3b9f3 commit 2c3f9b1
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 13 deletions.
4 changes: 3 additions & 1 deletion src/MystEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import TemplateManager from './components/TemplateManager';
import { TopbarButton } from './components/Buttons';
import Preview from './components/Preview';
import Diff from './components/Diff';
import { markdownReplacer } from './hooks/markdownReplacer';
import { markdownReplacer, useCustomRoles } from './hooks/markdownReplacer';

const EditorParent = styled.div`
display: flex;
Expand Down Expand Up @@ -126,6 +126,7 @@ const MystEditor = ({
templatelist,
collaboration = {},
spellcheckOpts = { dict: "en_US", dictionaryPath: "/dictionaries" },
customRoles = [],
transforms = []
}) => {
const [mode, setMode] = useState(initialMode);
Expand All @@ -139,6 +140,7 @@ const MystEditor = ({
markdownIt({ breaks: true, linkify: true })
.use(markdownitDocutils)
.use(markdownReplacer(transforms))
.use(useCustomRoles(customRoles))
.render(text)
)
}
Expand Down
79 changes: 67 additions & 12 deletions src/hooks/markdownReplacer.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import MarkdownIt from "markdown-it";
import { Role, rolePlugin } from "markdown-it-docutils";
/**
* @typedef {{
* target: RegExp,
* transform: function(string): string | Promise<string>
* target: string | RegExp,
* transform: (input: string) => string | Promise<string>
* }} Transform
*
* A transformation which will be applied to the output of `markdown-it`.
* `transform` will be applied to all matches of `target`.
*/

const cachePrefix = "myst-editor/"
const getCached = (key) => sessionStorage.getItem(cachePrefix + key)
const setCached = (key, value) => sessionStorage.setItem(cachePrefix + key, value)
const cachePrefix = "myst-editor/";
const getCached = (key) => sessionStorage.getItem(cachePrefix + key);
const setCached = (key, value) => sessionStorage.setItem(cachePrefix + key, value);

function waitForElementWithId(id) {
return new Promise(resolve => {
const observer = new MutationObserver(() => {
const elem = document.getElementById(id);
if (elem) {
observer.disconnect()
resolve(elem)
observer.disconnect();
resolve(elem);
};
});

Expand All @@ -31,7 +32,7 @@ function waitForElementWithId(id) {
}

const fillPlaceholder = (placeholderId, html) => {
const placeholder = document.getElementById(placeholderId)
const placeholder = document.getElementById(placeholderId);
if (placeholder) placeholder.outerHTML = html;
}

Expand All @@ -48,7 +49,7 @@ const cancelTransform = (placeholderId) => {
* @returns {string}
*/
const createTransformPlaceholder = (input, promise) => {
const placeholderId = Math.random().toString().slice(2);
const placeholderId = "placeholder-" + Math.random().toString().slice(2);

promise
.then(waitForElementWithId(placeholderId))
Expand All @@ -59,7 +60,7 @@ const createTransformPlaceholder = (input, promise) => {
.catch(err => {
console.error(err);
cancelTransform(placeholderId);
setCached(input, input)
setCached(input, input);
})

return `<span id="${placeholderId}">${input}</span>`
Expand All @@ -82,7 +83,7 @@ const overloadTransform = ({ transform: originalTransform, target }) => ({
if (typeof transformResult.then == "function") {
return createTransformPlaceholder(input, transformResult);
}

return transformResult;
}
})
Expand All @@ -109,6 +110,60 @@ const markdownReplacer =
};
}

/***************************** CUSTOM ROLES *****************************/

/**
* @typedef {{
* target: string,
* transform: (input: string) => string | Promise<string>
* }} RoleTransform
*
* A transformation which will be applied to the content of a MyST role specified as `target`
*/

const CUSTOM_ROLE_RULE = "custom_role";

/**
* @param {RoleTransform}
* @returns {{ name: string, role: Role }}
*/
const toDocutilsRole = ({ target, transform }) => {
const DocutilsRole = class extends Role {
run({ content }) {
const token = new this.state.Token(CUSTOM_ROLE_RULE, "span", 1);
token.content = transform(content);
return [token];
}
}

return { name: target, role: DocutilsRole }
}

/**
* @param { Transform[] }
* @returns {{ [rolename: string]: Role }}
*/
const asDocutilsRoles = (transforms) => transforms
.map(overloadTransform)
.map(toDocutilsRole)
.reduce((roles, {name, role}) => {roles[name] = role; return roles}, {});

/**
* @param { Transform[] } transforms
* @returns {function(MarkdownIt): void}
*/
const useCustomRoles =
(transforms) =>
(markdownIt) => {
const customRoles = asDocutilsRoles(transforms);

// Usually a markdownIt renderer rule would escape all html code. Here we create a rule
// which explicitly does nothing so that all html returned by transforms is rendered.
markdownIt.renderer.rules[CUSTOM_ROLE_RULE] = (tokens, idx) => tokens[idx].content;
markdownIt.use(rolePlugin, { roles: customRoles });
}

export {
markdownReplacer
markdownReplacer,
useCustomRoles
}

0 comments on commit 2c3f9b1

Please sign in to comment.