1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-24 08:12:24 +02:00

Simplified how Markdown-It plugins are created

This commit is contained in:
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') {
mdToHtmlOptions.plugins = {
checkbox: {
renderingType: 2,
checkboxRenderingType: 2,
},
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,8 @@ const md5 = require('md5');
interface RendererRule {
install(context:any, ruleOptions:any):any,
assets?(theme:any):any,
rule?: any, // TODO: remove
plugin?: any,
}
interface RendererRules {
@ -113,6 +115,15 @@ interface RenderResult {
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 {
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) {
const assets:any = {};
for (const key in rules) {
@ -303,17 +329,18 @@ export default class MdToHtml {
const cachedOutput = this.cachedOutputs_[cacheKey];
if (cachedOutput) return cachedOutput;
const context:PluginContext = {
css: {},
pluginAssets: {},
cache: this.contextCache_,
};
const ruleOptions = Object.assign({}, options, {
resourceBaseUrl: this.resourceBaseUrl_,
ResourceModel: this.ResourceModel_,
});
const context:PluginContext = {
css: {},
pluginAssets: {},
cache: this.contextCache_,
// options: ruleOptions,
};
const markdownIt = new MarkdownIt({
breaks: !this.pluginEnabled('softbreaks'),
typographer: this.pluginEnabled('typographer'),
@ -391,8 +418,18 @@ export default class MdToHtml {
for (const key in allRules) {
if (!this.pluginEnabled(key)) continue;
const rule = allRules[key];
const ruleInstall:Function = rule.install ? rule.install : (rule as any);
markdownIt.use(ruleInstall(context, { ...ruleOptions }));
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);
markdownIt.use(ruleInstall(context, { ...ruleOptions }));
}
}
markdownIt.use(markdownItAnchor, { slugify: slugify });
@ -410,11 +447,13 @@ export default class MdToHtml {
setupLinkify(markdownIt);
const renderedBody = markdownIt.render(body);
const renderedBody = markdownIt.render(body, context);
const pluginAssets = this.allUnprocessedAssets(theme);
let cssStrings = noteStyle(options.theme);
let output = this.processPluginAssets(context.pluginAssets);
let output = this.processPluginAssets(pluginAssets); // context.pluginAssets);
cssStrings = cssStrings.concat(output.cssStrings);
if (options.userCss) cssStrings.push(options.userCss);

View File

@ -1,24 +1,21 @@
import { RuleOptions } from '../../MdToHtml';
let checkboxIndex_ = -1;
const pluginAssets:Function[] = [];
pluginAssets[1] = function() {
function pluginAssets(theme:any) {
return [
{
inline: true,
mime: 'text/css',
text: `
/*
FOR THE MARKDOWN EDITOR
*/
/* 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
nested lists. Make sure this value matches the UL margin. */
/*
.md-checkbox .checkbox-wrapper {
display: flex;
align-items: center;
}
*/
li.md-checkbox {
list-style-type: none;
}
@ -26,30 +23,16 @@ pluginAssets[1] = function() {
li.md-checkbox input[type=checkbox] {
margin-left: -1.71em;
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 {
list-style:none;
}
/*
FOR THE RICH TEXT EDITOR
*/
ul.joplin-checklist li::before {
content:"\\f14a";
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[] {
let token = null;
@ -129,9 +112,8 @@ function createSuffixTokens(Token:any):any[] {
];
}
// @ts-ignore: Keep the function signature as-is despite unusued arguments
function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any) {
const pluginOptions = { renderingType: 1, ...ruleOptions.plugins['checkbox'] };
function checkboxPlugin(markdownIt:any, options:RuleOptions) {
const renderingType = options.checkboxRenderingType || 1;
markdownIt.core.ruler.push('checkbox', (state:any) => {
const tokens = state.tokens;
@ -180,14 +162,14 @@ function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any
const currentList = lists[lists.length - 1];
if (pluginOptions.renderingType === 1) {
if (renderingType === 1) {
checkboxIndex_++;
const id = `md-checkbox-${checkboxIndex_}`;
// 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.
const prefix = createPrefixTokens(Token, id, checked, label, ruleOptions.postMessageSyntax, token);
const prefix = createPrefixTokens(Token, id, checked, label, options.postMessageSyntax, token);
const suffix = createSuffixTokens(Token);
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());
}
}
if (!('checkbox' in context.pluginAssets)) {
context.pluginAssets['checkbox'] = pluginAssets[pluginOptions.renderingType](ruleOptions.theme);
}
}
}
});
}
export default {
install: function(context:any, ruleOptions:any) {
return function(md:any, mdOptions:any) {
installRule(md, mdOptions, ruleOptions, context);
};
},
assets: pluginAssets[2],
plugin: checkboxPlugin,
assets: pluginAssets,
};

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) {
return this.plugin.registerContentScript(type, id, scriptPath);
}

View File

@ -318,6 +318,25 @@ export type Path = string[];
// =================================================================
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',
CodeMirrorPlugin = 'codeMirrorPlugin',
}

View File

@ -8,8 +8,8 @@ import theme_solarizedDark from './themes/solarizedDark';
import theme_nord from './themes/nord';
import theme_aritimDark from './themes/aritimDark';
import theme_oledDark from './themes/oledDark';
import Setting from 'lib/models/Setting';
const Setting = require('lib/models/Setting').default;
const Color = require('color');
const themes:any = {
@ -364,13 +364,13 @@ function addExtraStyles(style:any) {
const themeCache_:any = {};
function themeStyle(theme:any) {
if (!theme) throw new Error('Theme must be specified');
function themeStyle(themeId:number) {
if (!themeId) throw new Error('Theme must be specified');
const zoomRatio = 1; // Setting.value('style.zoom') / 100;
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];
// 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
// relevant properties
output = Object.assign({}, globalStyle, fontSizes, themes[theme]);
output = Object.assign({}, globalStyle, fontSizes, themes[themeId]);
output = addMissingProperties(output);
output = addExtraStyles(output);
@ -406,7 +406,7 @@ const cachedStyles_:any = {
// cacheKey must be a globally unique key, and must change whenever
// the dependencies of the style change. If the style depends only
// 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;
// We clear the cache whenever switching themes