Simplified how Markdown-It plugins are created

plugin_content_scripts
Laurent Cozic 2020-10-20 17:52:02 +01:00
parent 47c7b864cb
commit fe90d92e01
11 changed files with 110 additions and 67 deletions

View File

@ -59,7 +59,7 @@ describe('MdToHtml', function() {
if (mdFilename === 'checkbox_alternative.md') { if (mdFilename === 'checkbox_alternative.md') {
mdToHtmlOptions.plugins = { mdToHtmlOptions.plugins = {
checkbox: { checkbox: {
renderingType: 2, checkboxRenderingType: 2,
}, },
}; };
} }

View File

@ -17,6 +17,6 @@ module.exports = function(pluginContext) {
installRule(md, mdOptions, ruleOptions, context); installRule(md, mdOptions, ruleOptions, context);
}; };
}, },
style: {}, assets: {},
} }
} }

View File

@ -17,6 +17,6 @@ module.exports = function(pluginContext) {
installRule(md, mdOptions, ruleOptions, context); installRule(md, mdOptions, ruleOptions, context);
}; };
}, },
style: {}, assets: {},
} }
} }

View File

@ -27,7 +27,7 @@ function markupRenderOptions(override:any = null) {
return { return {
plugins: { plugins: {
checkbox: { checkbox: {
renderingType: 2, checkboxRenderingType: 2,
}, },
link_open: { link_open: {
linkRenderingType: 2, linkRenderingType: 2,

View File

@ -10,7 +10,7 @@ export interface Props {
onMessage:Function, onMessage:Function,
pluginId:string, pluginId:string,
viewId:string, viewId:string,
themeId:string, themeId:number,
minWidth?: number, minWidth?: number,
minHeight?: number, minHeight?: number,
fitToContent?: boolean, fitToContent?: boolean,

View File

@ -6,7 +6,7 @@ const { camelCaseToDash, formatCssSize } = require('lib/string-utils');
interface HookDependencies { interface HookDependencies {
pluginId: string, pluginId: string,
themeId: string, themeId: number,
} }
function themeToCssVariables(theme:any) { function themeToCssVariables(theme:any) {

View File

@ -8,6 +8,8 @@ const md5 = require('md5');
interface RendererRule { interface RendererRule {
install(context:any, ruleOptions:any):any, install(context:any, ruleOptions:any):any,
assets?(theme:any):any, assets?(theme:any):any,
rule?: any, // TODO: remove
plugin?: any,
} }
interface RendererRules { interface RendererRules {
@ -113,6 +115,15 @@ interface RenderResult {
cssStrings: string[], cssStrings: string[],
} }
export interface RuleOptions {
context: PluginContext,
theme: any,
postMessageSyntax: string,
// Used by checkboxes to specify how it should be rendered
checkboxRenderingType?: number,
}
export default class MdToHtml { export default class MdToHtml {
private resourceBaseUrl_:string; private resourceBaseUrl_:string;
@ -235,6 +246,21 @@ export default class MdToHtml {
}; };
} }
private allUnprocessedAssets(theme:any) {
const assets:any = {};
for (const key in rules) {
if (!this.pluginEnabled(key)) continue;
const rule = rules[key];
if (rule.assets) {
assets[key] = rule.assets(theme);
}
}
return assets;
}
// TODO: remove
async allAssets(theme:any) { async allAssets(theme:any) {
const assets:any = {}; const assets:any = {};
for (const key in rules) { for (const key in rules) {
@ -303,17 +329,18 @@ export default class MdToHtml {
const cachedOutput = this.cachedOutputs_[cacheKey]; const cachedOutput = this.cachedOutputs_[cacheKey];
if (cachedOutput) return cachedOutput; if (cachedOutput) return cachedOutput;
const context:PluginContext = {
css: {},
pluginAssets: {},
cache: this.contextCache_,
};
const ruleOptions = Object.assign({}, options, { const ruleOptions = Object.assign({}, options, {
resourceBaseUrl: this.resourceBaseUrl_, resourceBaseUrl: this.resourceBaseUrl_,
ResourceModel: this.ResourceModel_, ResourceModel: this.ResourceModel_,
}); });
const context:PluginContext = {
css: {},
pluginAssets: {},
cache: this.contextCache_,
// options: ruleOptions,
};
const markdownIt = new MarkdownIt({ const markdownIt = new MarkdownIt({
breaks: !this.pluginEnabled('softbreaks'), breaks: !this.pluginEnabled('softbreaks'),
typographer: this.pluginEnabled('typographer'), typographer: this.pluginEnabled('typographer'),
@ -391,9 +418,19 @@ export default class MdToHtml {
for (const key in allRules) { for (const key in allRules) {
if (!this.pluginEnabled(key)) continue; if (!this.pluginEnabled(key)) continue;
const rule = allRules[key]; const rule = allRules[key];
if (rule.plugin) {
const pluginOptions = {
context: context,
...ruleOptions,
...(ruleOptions.plugins[key] ? ruleOptions.plugins[key] : {}),
};
markdownIt.use(rule.plugin, pluginOptions);
} else {
const ruleInstall:Function = rule.install ? rule.install : (rule as any); const ruleInstall:Function = rule.install ? rule.install : (rule as any);
markdownIt.use(ruleInstall(context, { ...ruleOptions })); markdownIt.use(ruleInstall(context, { ...ruleOptions }));
} }
}
markdownIt.use(markdownItAnchor, { slugify: slugify }); markdownIt.use(markdownItAnchor, { slugify: slugify });
@ -410,11 +447,13 @@ export default class MdToHtml {
setupLinkify(markdownIt); setupLinkify(markdownIt);
const renderedBody = markdownIt.render(body); const renderedBody = markdownIt.render(body, context);
const pluginAssets = this.allUnprocessedAssets(theme);
let cssStrings = noteStyle(options.theme); let cssStrings = noteStyle(options.theme);
let output = this.processPluginAssets(context.pluginAssets); let output = this.processPluginAssets(pluginAssets); // context.pluginAssets);
cssStrings = cssStrings.concat(output.cssStrings); cssStrings = cssStrings.concat(output.cssStrings);
if (options.userCss) cssStrings.push(options.userCss); if (options.userCss) cssStrings.push(options.userCss);

View File

@ -1,24 +1,21 @@
import { RuleOptions } from '../../MdToHtml';
let checkboxIndex_ = -1; let checkboxIndex_ = -1;
const pluginAssets:Function[] = []; function pluginAssets(theme:any) {
pluginAssets[1] = function() {
return [ return [
{ {
inline: true, inline: true,
mime: 'text/css', mime: 'text/css',
text: ` text: `
/*
FOR THE MARKDOWN EDITOR
*/
/* Remove the indentation from the checkboxes at the root of the document /* Remove the indentation from the checkboxes at the root of the document
(otherwise they are too far right), but keep it for their children to allow (otherwise they are too far right), but keep it for their children to allow
nested lists. Make sure this value matches the UL margin. */ nested lists. Make sure this value matches the UL margin. */
/*
.md-checkbox .checkbox-wrapper {
display: flex;
align-items: center;
}
*/
li.md-checkbox { li.md-checkbox {
list-style-type: none; list-style-type: none;
} }
@ -26,30 +23,16 @@ pluginAssets[1] = function() {
li.md-checkbox input[type=checkbox] { li.md-checkbox input[type=checkbox] {
margin-left: -1.71em; margin-left: -1.71em;
margin-right: 0.7em; margin-right: 0.7em;
}`,
},
];
};
pluginAssets[2] = function(theme:any) {
return [
{
inline: true,
mime: 'text/css',
text: `
/* https://stackoverflow.com/questions/7478336/only-detect-click-event-on-pseudo-element#comment39751366_7478344 */
/* Not doing this trick anymore. See Modules/TinyMCE/JoplinLists/src/main/ts/ui/Buttons.ts */
/*
ul.joplin-checklist li {
pointer-events: none;
} }
*/
ul.joplin-checklist { ul.joplin-checklist {
list-style:none; list-style:none;
} }
/*
FOR THE RICH TEXT EDITOR
*/
ul.joplin-checklist li::before { ul.joplin-checklist li::before {
content:"\\f14a"; content:"\\f14a";
font-family:"Font Awesome 5 Free"; font-family:"Font Awesome 5 Free";
@ -68,7 +51,7 @@ pluginAssets[2] = function(theme:any) {
}`, }`,
}, },
]; ];
}; }
function createPrefixTokens(Token:any, id:string, checked:boolean, label:string, postMessageSyntax:string, sourceToken:any):any[] { function createPrefixTokens(Token:any, id:string, checked:boolean, label:string, postMessageSyntax:string, sourceToken:any):any[] {
let token = null; let token = null;
@ -129,9 +112,8 @@ function createSuffixTokens(Token:any):any[] {
]; ];
} }
// @ts-ignore: Keep the function signature as-is despite unusued arguments function checkboxPlugin(markdownIt:any, options:RuleOptions) {
function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any) { const renderingType = options.checkboxRenderingType || 1;
const pluginOptions = { renderingType: 1, ...ruleOptions.plugins['checkbox'] };
markdownIt.core.ruler.push('checkbox', (state:any) => { markdownIt.core.ruler.push('checkbox', (state:any) => {
const tokens = state.tokens; const tokens = state.tokens;
@ -180,14 +162,14 @@ function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any
const currentList = lists[lists.length - 1]; const currentList = lists[lists.length - 1];
if (pluginOptions.renderingType === 1) { if (renderingType === 1) {
checkboxIndex_++; checkboxIndex_++;
const id = `md-checkbox-${checkboxIndex_}`; const id = `md-checkbox-${checkboxIndex_}`;
// Prepend the text content with the checkbox markup and the opening <label> tag // Prepend the text content with the checkbox markup and the opening <label> tag
// then append the </label> tag at the end of the text content. // then append the </label> tag at the end of the text content.
const prefix = createPrefixTokens(Token, id, checked, label, ruleOptions.postMessageSyntax, token); const prefix = createPrefixTokens(Token, id, checked, label, options.postMessageSyntax, token);
const suffix = createSuffixTokens(Token); const suffix = createSuffixTokens(Token);
token.children = markdownIt.utils.arrayReplaceAt(token.children, 0, prefix); token.children = markdownIt.utils.arrayReplaceAt(token.children, 0, prefix);
@ -214,20 +196,12 @@ function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any
currentListItem.attrSet('class', (`${currentListItem.attrGet('class') || ''} checked`).trim()); currentListItem.attrSet('class', (`${currentListItem.attrGet('class') || ''} checked`).trim());
} }
} }
if (!('checkbox' in context.pluginAssets)) {
context.pluginAssets['checkbox'] = pluginAssets[pluginOptions.renderingType](ruleOptions.theme);
}
} }
} }
}); });
} }
export default { export default {
install: function(context:any, ruleOptions:any) { plugin: checkboxPlugin,
return function(md:any, mdOptions:any) { assets: pluginAssets,
installRule(md, mdOptions, ruleOptions, context);
};
},
assets: pluginAssets[2],
}; };

View File

@ -49,6 +49,17 @@ export default class JoplinPlugins {
} }
} }
/**
* Registers a new content script. Unlike regular plugin code, which runs in a separate process, content scripts run within the main process code
* and thus allow improved performances and more customisations in specific cases. It can be used for example to load a Markdown or editor plugin.
*
* Note that registering a content script in itself will do nothing - it will only be loaded in specific cases by the relevant app modules
* (eg. the Markdown renderer or the code editor). So it is not a way to inject and run arbitrary code in the app, which for safety and performance reasons is not supported.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins/content_script)
*
* @param scriptPath Must be a path relative to the plugin main script. For example, if your file content_script.js is next to your index.ts file, you would set `scriptPath` to `"./content_script.js`.
*/
async registerContentScript(type:ContentScriptType, id:string, scriptPath:string) { async registerContentScript(type:ContentScriptType, id:string, scriptPath:string) {
return this.plugin.registerContentScript(type, id, scriptPath); return this.plugin.registerContentScript(type, id, scriptPath);
} }

View File

@ -318,6 +318,25 @@ export type Path = string[];
// ================================================================= // =================================================================
export enum ContentScriptType { export enum ContentScriptType {
/**
* Registers a new Markdown-It plugin, which should follow this template:
*
* ```javascript
* // The module should export a function that takes a `pluginContext` as argument (currently unused)
* module.exports = function(pluginContext) {
* // That function should return an object with a number of properties:
* return {
* // Required:
* install: function(context, ruleOptions) {
* return function(md, mdOptions) {
* installRule(md, mdOptions, ruleOptions, context);
* };
* },
* assets: {},
* }
* }
* ```
*/
MarkdownItPlugin = 'markdownItPlugin', MarkdownItPlugin = 'markdownItPlugin',
CodeMirrorPlugin = 'codeMirrorPlugin', CodeMirrorPlugin = 'codeMirrorPlugin',
} }

View File

@ -8,8 +8,8 @@ import theme_solarizedDark from './themes/solarizedDark';
import theme_nord from './themes/nord'; import theme_nord from './themes/nord';
import theme_aritimDark from './themes/aritimDark'; import theme_aritimDark from './themes/aritimDark';
import theme_oledDark from './themes/oledDark'; import theme_oledDark from './themes/oledDark';
import Setting from 'lib/models/Setting';
const Setting = require('lib/models/Setting').default;
const Color = require('color'); const Color = require('color');
const themes:any = { const themes:any = {
@ -364,13 +364,13 @@ function addExtraStyles(style:any) {
const themeCache_:any = {}; const themeCache_:any = {};
function themeStyle(theme:any) { function themeStyle(themeId:number) {
if (!theme) throw new Error('Theme must be specified'); if (!themeId) throw new Error('Theme must be specified');
const zoomRatio = 1; // Setting.value('style.zoom') / 100; const zoomRatio = 1; // Setting.value('style.zoom') / 100;
const editorFontSize = Setting.value('style.editor.fontSize'); const editorFontSize = Setting.value('style.editor.fontSize');
const cacheKey = [theme, zoomRatio, editorFontSize].join('-'); const cacheKey = [themeId, zoomRatio, editorFontSize].join('-');
if (themeCache_[cacheKey]) return themeCache_[cacheKey]; if (themeCache_[cacheKey]) return themeCache_[cacheKey];
// Font size are not theme specific, but they must be referenced // Font size are not theme specific, but they must be referenced
@ -390,7 +390,7 @@ function themeStyle(theme:any) {
// All theme are based on the light style, and just override the // All theme are based on the light style, and just override the
// relevant properties // relevant properties
output = Object.assign({}, globalStyle, fontSizes, themes[theme]); output = Object.assign({}, globalStyle, fontSizes, themes[themeId]);
output = addMissingProperties(output); output = addMissingProperties(output);
output = addExtraStyles(output); output = addExtraStyles(output);
@ -406,7 +406,7 @@ const cachedStyles_:any = {
// cacheKey must be a globally unique key, and must change whenever // cacheKey must be a globally unique key, and must change whenever
// the dependencies of the style change. If the style depends only // the dependencies of the style change. If the style depends only
// on the theme, a static string can be provided as a cache key. // on the theme, a static string can be provided as a cache key.
function buildStyle(cacheKey:any, themeId:string, callback:Function) { function buildStyle(cacheKey:any, themeId:number, callback:Function) {
cacheKey = Array.isArray(cacheKey) ? cacheKey.join('_') : cacheKey; cacheKey = Array.isArray(cacheKey) ? cacheKey.join('_') : cacheKey;
// We clear the cache whenever switching themes // We clear the cache whenever switching themes