Skip to content

Commit

Permalink
Adapt Plugin and PluginManager to TypeScript
Browse files Browse the repository at this point in the history
  • Loading branch information
jovyntls committed Feb 11, 2023
1 parent b79f954 commit 1ce0924
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 77 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"@types/jest": "^27.4.1",
"@types/js-beautify": "^1.13.3",
"@types/lodash": "^4.14.181",
"@types/node": "^18.11.19",
"@types/nunjucks": "^3.2.1",
"@types/path-is-inside": "^1.0.0",
"@types/url-parse": "^1.4.8",
Expand Down
30 changes: 16 additions & 14 deletions packages/core/src/html/NodeProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ const FRONTMATTER_FENCE = '---';

cheerio.prototype.options.decodeEntities = false; // Don't escape HTML entities

export type NodeProcessorConfig = {
baseUrl: string,
baseUrlMap: Set<string>,
rootPath: string,
outputPath: string,
ignore: string[],
addressablePagesSource: string[],
intrasiteLinkValidation: { enabled: boolean },
codeLineNumbers: boolean,
plantumlCheck: boolean,
headerIdMap: {
[id: string]: number,
},
};

export class NodeProcessor {
frontmatter: { [key: string]: string } = {};

Expand All @@ -54,20 +69,7 @@ export class NodeProcessor {
processedModals: { [id: string]: boolean } = {};

constructor(
private config: {
baseUrl: string,
baseUrlMap: Set<string>,
rootPath: string,
outputPath: string,
ignore: string[],
addressablePagesSource: string[],
intrasiteLinkValidation: { enabled: boolean },
codeLineNumbers: boolean,
plantumlCheck: boolean,
headerIdMap: {
[id: string]: number,
},
},
private config: NodeProcessorConfig,
private pageSources: PageSources,
private variableProcessor: VariableProcessor,
private pluginManager: any,
Expand Down
84 changes: 59 additions & 25 deletions packages/core/src/plugins/Plugin.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,66 @@
const path = require('path');
const fs = require('fs-extra');
const cheerio = require('cheerio'); require('../patches/htmlparser2');
import path from 'path';
import fs from 'fs-extra';
import cheerio from 'cheerio';

import { DomElement } from 'htmlparser2';
import isString from 'lodash/isString';
import * as logger from '../utils/logger';
import * as urlUtil from '../utils/urlUtil';
import { NodeProcessorConfig } from '../html/NodeProcessor';

require('../patches/htmlparser2');

const _ = { isString };

const PLUGIN_OUTPUT_SITE_ASSET_FOLDER_NAME = 'plugins';

const logger = require('../utils/logger');
const urlUtil = require('../utils/urlUtil');
export interface PluginContext {
[key: string]: any;
}

export interface FrontMatter {
[key: string]: any;
}

type TagConfigAttributes = {
name: string,
isRelative: boolean,
isSourceFile: boolean
};

export type TagConfigs = {
isSpecial: boolean,
attributes: TagConfigAttributes[]
};

/**
* Wrapper class around a loaded plugin module
*/
class Plugin {
constructor(pluginName, pluginPath, pluginOptions, siteOutputPath) {
export class Plugin {
pluginName: string;
plugin: {
beforeSiteGenerate: (...args: any[]) => any;
getLinks: (...args: any[]) => any;
getScripts: (...args: any[]) => any;
postRender: (pluginContext: PluginContext, frontmatter: FrontMatter, content: string) => string;
processNode: (pluginContext: PluginContext, node: DomElement, config?: NodeProcessorConfig) => string;
postProcessNode: (pluginContext: PluginContext, node: DomElement, config?: NodeProcessorConfig) => string;
tagConfig: { [key: string]: TagConfigs };
};

pluginOptions: PluginContext;
pluginAbsolutePath: string;
pluginAssetOutputPath: string;

constructor(pluginName: string, pluginPath: string, pluginOptions: PluginContext, siteOutputPath: string) {
this.pluginName = pluginName;

/**
* The plugin module
* @type {Object}
*/
// eslint-disable-next-line global-require,import/no-dynamic-require
this.plugin = require(pluginPath);

/**
* @type {Object<string, any>}
*/
this.pluginOptions = pluginOptions || {};

// For resolving plugin asset source paths later
Expand All @@ -49,12 +86,12 @@ class Plugin {
* @param baseUrl baseUrl of the site
* @return String html of the element, with the attribute's asset resolved
*/
_getResolvedAssetElement(assetElementHtml, tagName, attrName, baseUrl) {
_getResolvedAssetElement(assetElementHtml: string, tagName: string, attrName: string, baseUrl: string) {
const $ = cheerio.load(assetElementHtml);
const el = $(`${tagName}[${attrName}]`);

el.attr(attrName, (i, assetPath) => {
if (!assetPath || urlUtil.isUrl(assetPath)) {
el.attr(attrName, (_i: any, assetPath: any): string => {
if (!assetPath || !_.isString(assetPath) || urlUtil.isUrl(assetPath)) {
return assetPath;
}

Expand All @@ -79,19 +116,20 @@ class Plugin {
/**
* Collect page content inserted by plugins
*/
getPageNjkLinksAndScripts(frontmatter, content, baseUrl) {
getPageNjkLinksAndScripts(frontmatter: FrontMatter, content: string, baseUrl: string) {
let links = [];
let scripts = [];

if (this.plugin.getLinks) {
const pluginLinks = this.plugin.getLinks(this.pluginOptions, frontmatter, content);
links = pluginLinks.map(linkHtml => this._getResolvedAssetElement(linkHtml, 'link', 'href', baseUrl));
links = pluginLinks.map(
(linkHtml: string) => this._getResolvedAssetElement(linkHtml, 'link', 'href', baseUrl));
}

if (this.plugin.getScripts) {
const pluginScripts = this.plugin.getScripts(this.pluginOptions, frontmatter, content);
scripts = pluginScripts.map(scriptHtml => this._getResolvedAssetElement(scriptHtml, 'script',
'src', baseUrl));
scripts = pluginScripts.map((scriptHtml: string) => this._getResolvedAssetElement(scriptHtml, 'script',
'src', baseUrl));
}

return {
Expand All @@ -100,22 +138,22 @@ class Plugin {
};
}

postRender(frontmatter, content) {
postRender(frontmatter: FrontMatter, content: string) {
if (this.plugin.postRender) {
return this.plugin.postRender(this.pluginOptions, frontmatter, content);
}
return content;
}

processNode(node, config) {
processNode(node: DomElement, config: NodeProcessorConfig) {
if (!this.plugin.processNode) {
return;
}

this.plugin.processNode(this.pluginOptions, node, config);
}

postProcessNode(node, config) {
postProcessNode(node: DomElement, config: NodeProcessorConfig) {
if (!this.plugin.postProcessNode) {
return;
}
Expand All @@ -127,7 +165,3 @@ class Plugin {
return this.plugin.tagConfig;
}
}

module.exports = {
Plugin,
};
90 changes: 57 additions & 33 deletions packages/core/src/plugins/PluginManager.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,60 @@
const path = require('path');
const fs = require('fs-extra');
const walkSync = require('walk-sync');
import merge from 'lodash/merge';

import { DomElement } from 'htmlparser2';
import path from 'path';
import fs from 'fs-extra';
import walkSync from 'walk-sync';
import flatMap from 'lodash/flatMap';
import get from 'lodash/get';
import includes from 'lodash/includes';
import isError from 'lodash/isError';
import * as logger from '../utils/logger';
import {
FrontMatter, Plugin, PluginContext, TagConfigs,
} from './Plugin';
import { NodeProcessorConfig } from '../html/NodeProcessor';

const { ignoreTags } = require('../patches');

const { Plugin } = require('./Plugin');

const _ = {};
_.flatMap = require('lodash/flatMap');
_.get = require('lodash/get');
_.includes = require('lodash/includes');
_.merge = require('lodash/merge');

const logger = require('../utils/logger');
const _ = {
flatMap,
get,
includes,
isError,
merge,
};

const MARKBIND_PLUGIN_DIRECTORY = __dirname;
const MARKBIND_DEFAULT_PLUGIN_DIRECTORY = path.join(__dirname, 'default');
const MARKBIND_PLUGIN_PREFIX = 'markbind-plugin-';
const PROJECT_PLUGIN_FOLDER_NAME = '_markbind/plugins';

class PluginManager {
constructor(config, plugins, pluginsContext) {
type PageAsset = {
pluginScripts: string[],
pluginLinks: string[],
};

export class PluginManager {
static tagConfig: { [key: string]: TagConfigs };

config: NodeProcessorConfig;
plugins: { [key: string]: Plugin };
pluginsRaw: string[];
pluginsContextRaw: PluginContext;
htmlBeautifyOptions: { [key: string]: any };

constructor(config: NodeProcessorConfig, plugins: string[], pluginsContext: PluginContext) {
this.config = config;

/**
* @type {Object<string, Plugin>}
*/
this.plugins = {};

/**
* Raw array of plugin names as read from the site configuration
* @type {Array}
*/
this.pluginsRaw = plugins;

/**
* Raw representation of the site configuration's plugisnContext key
* @type {Object<string, Object<string, any>>}
*/
this.pluginsContextRaw = pluginsContext;

Expand All @@ -55,8 +73,6 @@ class PluginManager {
* Load all plugins of the site
*/
_collectPlugins() {
module.paths.push(path.join(this.config.rootPath, 'node_modules'));

const defaultPluginNames = walkSync(MARKBIND_DEFAULT_PLUGIN_DIRECTORY, {
directories: false,
globs: [`${MARKBIND_PLUGIN_PREFIX}*.js`],
Expand All @@ -78,7 +94,7 @@ class PluginManager {
* @param plugin name of the plugin
* @param isDefault whether the plugin is a default plugin
*/
_loadPlugin(plugin, isDefault) {
_loadPlugin(plugin: string, isDefault: boolean) {
try {
// Check if already loaded
if (this.plugins[plugin]) {
Expand Down Expand Up @@ -108,7 +124,7 @@ class PluginManager {
* @param projectRootPath root of the MarkBind project
* @param pluginName name of the plugin
*/
static _getPluginPath(projectRootPath, pluginName) {
static _getPluginPath(projectRootPath: string, pluginName: string) {
// Check in project folder
const pluginPath = path.join(projectRootPath, PROJECT_PLUGIN_FOLDER_NAME, `${pluginName}.js`);
if (fs.existsSync(pluginPath)) {
Expand All @@ -127,7 +143,19 @@ class PluginManager {
return markbindDefaultPluginPath;
}

return require.resolve(pluginName);
// Check the environment's node_modules folders
try {
const resolvedPluginPath = require.resolve(pluginName);
return resolvedPluginPath;
} catch (err) {
// An error may be thrown because the module is not found, or for other reasons.
// If the error is due to MODULE_NOT_FOUND, search project's node_modules
if (_.isError(err) && (err as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') {
return require.resolve(pluginName, { paths: [path.join(projectRootPath, 'node_modules')] });
}
// Re-throw all other errors
throw err;
}
}

/**
Expand All @@ -142,7 +170,7 @@ class PluginManager {
return;
}

Object.entries(pluginTagConfig).forEach(([tagName, tagConfig]) => {
Object.entries(pluginTagConfig).forEach(([tagName, tagConfig]: [string, TagConfigs]) => {
if (tagConfig.isSpecial) {
specialTags.add(tagName.toLowerCase());
}
Expand All @@ -168,26 +196,26 @@ class PluginManager {
/**
* Run getLinks and getScripts hooks
*/
collectPluginPageNjkAssets(frontmatter, content, pageAsset) {
collectPluginPageNjkAssets(frontmatter: FrontMatter, content: string, pageAsset: PageAsset) {
const pluginLinksAndScripts = Object.values(this.plugins)
.map(plugin => plugin.getPageNjkLinksAndScripts(frontmatter, content, this.config.baseUrl));

pageAsset.pluginLinks = _.flatMap(pluginLinksAndScripts, pluginResult => pluginResult.links);
pageAsset.pluginScripts = _.flatMap(pluginLinksAndScripts, pluginResult => pluginResult.scripts);
}

postRender(frontmatter, content) {
postRender(frontmatter: FrontMatter, content: string) {
return Object.values(this.plugins)
.reduce((renderedContent, plugin) => plugin.postRender(frontmatter, renderedContent), content);
}

processNode(node) {
processNode(node: DomElement) {
Object.values(this.plugins).forEach((plugin) => {
plugin.processNode(node, this.config);
});
}

postProcessNode(node) {
postProcessNode(node: DomElement) {
Object.values(this.plugins).forEach((plugin) => {
plugin.postProcessNode(node, this.config);
});
Expand All @@ -196,7 +224,3 @@ class PluginManager {

// Static property for easy access in linkProcessor
PluginManager.tagConfig = {};

module.exports = {
PluginManager,
};
2 changes: 1 addition & 1 deletion packages/core/src/plugins/codeBlockCopyButtons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
doesFunctionBtnContainerExistInNode,
isFunctionBtnContainer,
} from './codeBlockButtonsAssets/codeBlockButtonsContainer';
import { PluginContext } from './types';
import { PluginContext } from './Plugin';

const COPY_ICON = `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/plugins/codeBlockWrapButtons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
isFunctionBtnContainer,
doesFunctionBtnContainerExistInNode,
} from './codeBlockButtonsAssets/codeBlockButtonsContainer';
import { PluginContext } from './types';
import { PluginContext } from './Plugin';

const WRAP_ICON = `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
Expand Down
Loading

0 comments on commit 1ce0924

Please sign in to comment.