New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

markbind

Package Overview
Dependencies
Maintainers
2
Versions
28
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

markbind - npm Package Compare versions

Comparing version
1.7.0
to
2.9.0
+15
src/constants.js
module.exports = {
// src/lib/markbind/src/parser.js
ATTRIB_INCLUDE_PATH: 'include-path',
ATTRIB_CWF: 'cwf',
BOILERPLATE_FOLDER_NAME: '_markbind/boilerplates',
/* Imported global variables will be assigned a namespace.
* A prefix is appended to reduce clashes with other variables in the page.
*/
IMPORTED_VARIABLE_PREFIX: '$__MARKBIND__',
// src/lib/markbind/src/utils.js
markdownFileExts: ['md', 'mbd', 'mbdf'],
};
class CyclicReferenceError extends Error {
constructor(callStack) {
super();
const fileStack = callStack.slice(Math.max(callStack.length - 5, 0));
this.message = 'Cyclic reference detected.\n'
+ `Last 5 files processed:${'\n\t'}${fileStack.join('\n\t')}`;
}
}
CyclicReferenceError.MAX_RECURSIVE_DEPTH = 200;
module.exports = CyclicReferenceError;
const hljs = require('highlight.js');
const markdownIt = require('markdown-it')({
html: true,
linkify: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre><code class="hljs ' + lang + '">' +
hljs.highlight(lang, str, true).value +
'</code></pre>';
} catch (__) {
}
}
return '<pre><code class="hljs">' + markdownIt.utils.escapeHtml(str) + '</code></pre>';
}
});
const slugify = require('@sindresorhus/slugify');
// markdown-it plugins
markdownIt.use(require('markdown-it-mark'))
.use(require('markdown-it-ins'))
.use(require('markdown-it-anchor'), {slugify: (str) => slugify(str, { decamelize: false })})
.use(require('markdown-it-imsize'), {autofill: false})
.use(require('markdown-it-table-of-contents'))
.use(require('markdown-it-task-lists'), {enabled: true})
.use(require('markdown-it-linkify-images'), {imgClass: 'img-fluid'})
.use(require('markdown-it-attrs'))
.use(require('./markdown-it-dimmed'))
.use(require('./markdown-it-radio-button'))
.use(require('./markdown-it-block-embed'))
.use(require('./markdown-it-icons'))
.use(require('./markdown-it-footnotes'));
// fix link
markdownIt.normalizeLink = require('./normalizeLink');
// fix table style
markdownIt.renderer.rules.table_open = (tokens, idx) => {
return '<div class="table-responsive"><table class="markbind-table table table-bordered table-striped">';
};
markdownIt.renderer.rules.table_close = (tokens, idx) => {
return '</table></div>';
};
// highlight inline code
markdownIt.renderer.rules.code_inline = (tokens, idx, options, env, slf) => {
const token = tokens[idx];
const lang = token.attrGet('class');
if (lang && hljs.getLanguage(lang)) {
token.attrSet('class', `hljs inline ${lang}`);
return '<code' + slf.renderAttrs(token) + '>'
+ hljs.highlight(lang, token.content, true).value
+ '</code>';
} else {
return '<code' + slf.renderAttrs(token) + '>'
+ markdownIt.utils.escapeHtml(token.content)
+ '</code>';
}
};
// fix emoji numbers
const emojiData = require('markdown-it-emoji/lib/data/full.json');
// Extend emoji here
emojiData['zero'] = emojiData['0'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0030-20e3.png">';
emojiData['one'] = emojiData['1'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0031-20e3.png">';
emojiData['two'] = emojiData['2'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0032-20e3.png">';
emojiData['three'] = emojiData['3'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0033-20e3.png">';
emojiData['four'] = emojiData['4'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0034-20e3.png">';
emojiData['five'] = emojiData['5'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0035-20e3.png">';
emojiData['six'] = emojiData['6'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0036-20e3.png">';
emojiData['seven'] = emojiData['7'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0037-20e3.png">';
emojiData['eight'] = emojiData['8'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0038-20e3.png">';
emojiData['nine'] = emojiData['9'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0039-20e3.png">';
markdownIt.use(require('markdown-it-emoji'), {
defs: emojiData
});
module.exports = markdownIt;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
const PluginEnvironment = require("./PluginEnvironment");
const renderer = require("./renderer");
const tokenizer = require("./tokenizer");
function setup(md, options) {
let env = new PluginEnvironment(md, options);
md.block.ruler.before("fence", "video", tokenizer.bind(env), {
alt: [ "paragraph", "reference", "blockquote", "list" ]
});
md.renderer.rules["video"] = renderer.bind(env);
}
module.exports = setup;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
'use strict';
const YouTubeService = require('./services/YouTubeService');
const VimeoService = require('./services/VimeoService');
const VineService = require('./services/VineService');
const PreziService = require('./services/PreziService');
const SlideShareService = require('./services/SlideShareService');
const PowerPointOnlineService = require('./services/PowerPointOnlineService');
class PluginEnvironment {
constructor(md, options) {
this.md = md;
this.options = Object.assign(this.getDefaultOptions(), options);
this._initServices();
}
_initServices() {
let defaultServiceBindings = {
'youtube': YouTubeService,
'vimeo': VimeoService,
'vine': VineService,
'prezi': PreziService,
'slideshare': SlideShareService,
'powerpoint': PowerPointOnlineService,
};
let serviceBindings = Object.assign({}, defaultServiceBindings, this.options.services);
let services = {};
for (let serviceName of Object.keys(serviceBindings)) {
let _serviceClass = serviceBindings[serviceName];
services[serviceName] = new _serviceClass(serviceName, this.options[serviceName], this);
}
this.services = services;
}
getDefaultOptions() {
return {
containerClassName: 'block-embed',
serviceClassPrefix: 'block-embed-service-',
outputPlayerSize: true,
allowFullScreen: true,
filterUrl: null
};
}
}
module.exports = PluginEnvironment;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
function renderer(tokens, idx, options, _env) {
let videoToken = tokens[idx];
let service = videoToken.info.service;
let videoID = videoToken.info.videoID;
return service.getEmbedCode(videoID);
}
module.exports = renderer;
"use strict";
const VideoServiceBase = require("./VideoServiceBase");
class PowerPointOnlineService extends VideoServiceBase {
getDefaultOptions() {
return {width: 610, height: 481};
}
extractVideoID(reference) {
return reference;
}
getVideoUrl(serviceUrl) {
return `${serviceUrl}&action=embedview&wdAr=1.3333333333333333`;
}
}
module.exports = PowerPointOnlineService;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
const VideoServiceBase = require("./VideoServiceBase");
class PreziService extends VideoServiceBase {
getDefaultOptions() {
return { width: 550, height: 400 };
}
extractVideoID(reference) {
let match = reference.match(/^https:\/\/prezi.com\/(.[^/]+)/);
return match ? match[1] : reference;
}
getVideoUrl(videoID) {
let escapedVideoID = this.env.md.utils.escapeHtml(videoID);
return "https://prezi.com/embed/" + escapedVideoID
+ "/?bgcolor=ffffff&amp;lock_to_path=0&amp;autoplay=0&amp;autohide_ctrls=0&amp;"
+ "landing_data=bHVZZmNaNDBIWnNjdEVENDRhZDFNZGNIUE43MHdLNWpsdFJLb2ZHanI5N1lQVHkxSHFxazZ0UUNCRHloSXZROHh3PT0&amp;"
+ "landing_sign=1kD6c0N6aYpMUS0wxnQjxzSqZlEB8qNFdxtdjYhwSuI";
}
}
module.exports = PreziService;
"use strict";
const VideoServiceBase = require("./VideoServiceBase");
class SlideShareService extends VideoServiceBase {
getDefaultOptions() {
return {width: 599, height: 487};
}
extractVideoID(reference) {
return reference;
}
getVideoUrl(videoID) {
let escapedVideoID = this.env.md.utils.escapeHtml(videoID);
return `//www.slideshare.net/slideshow/embed_code/key/${escapedVideoID}`;
}
}
module.exports = SlideShareService;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
function defaultUrlFilter(url, _videoID, _serviceName, _options) {
return url;
}
class VideoServiceBase {
constructor(name, options, env) {
this.name = name;
this.options = Object.assign(this.getDefaultOptions(), options);
this.env = env;
}
getDefaultOptions() {
return {};
}
extractVideoID(reference) {
return reference;
}
getVideoUrl(_videoID) {
throw new Error("not implemented");
}
getFilteredVideoUrl(videoID) {
let filterUrlDelegate = typeof this.env.options.filterUrl === "function"
? this.env.options.filterUrl
: defaultUrlFilter;
let videoUrl = this.getVideoUrl(videoID);
return filterUrlDelegate(videoUrl, this.name, videoID, this.env.options);
}
getEmbedCode(videoID) {
let containerClassNames = [];
if (this.env.options.containerClassName) {
containerClassNames.push(this.env.options.containerClassName);
}
let escapedServiceName = this.env.md.utils.escapeHtml(this.name);
containerClassNames.push(this.env.options.serviceClassPrefix + escapedServiceName);
let iframeAttributeList = [];
iframeAttributeList.push([ "type", "text/html" ]);
iframeAttributeList.push([ "src", this.getFilteredVideoUrl(videoID) ]);
iframeAttributeList.push([ "frameborder", 0 ]);
if (this.env.options.outputPlayerSize === true) {
if (this.options.width !== undefined && this.options.width !== null) {
iframeAttributeList.push([ "width", this.options.width ]);
}
if (this.options.height !== undefined && this.options.height !== null) {
iframeAttributeList.push([ "height", this.options.height ]);
}
}
if (this.env.options.allowFullScreen === true) {
iframeAttributeList.push([ "webkitallowfullscreen" ]);
iframeAttributeList.push([ "mozallowfullscreen" ]);
iframeAttributeList.push([ "allowfullscreen" ]);
}
let iframeAttributes = iframeAttributeList
.map(pair =>
pair[1] !== undefined
? `${pair[0]}="${pair[1]}"`
: pair[0]
)
.join(" ");
return `<div class="${containerClassNames.join(" ")}">`
+ `<iframe ${iframeAttributes}></iframe>`
+ `</div>\n`;
}
}
module.exports = VideoServiceBase;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
const VideoServiceBase = require("./VideoServiceBase");
class VimeoService extends VideoServiceBase {
getDefaultOptions() {
return { width: 500, height: 281 };
}
extractVideoID(reference) {
let match = reference.match(/https?:\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|)(\d+)(?:$|\/|\?)/);
return match && typeof match[3] === "string" ? match[3] : reference;
}
getVideoUrl(videoID) {
let escapedVideoID = this.env.md.utils.escapeHtml(videoID);
return `//player.vimeo.com/video/${escapedVideoID}`;
}
}
module.exports = VimeoService;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
const VideoServiceBase = require("./VideoServiceBase");
class VineService extends VideoServiceBase {
getDefaultOptions() {
return { width: 600, height: 600, embed: "simple" };
}
extractVideoID(reference) {
let match = reference.match(/^http(?:s?):\/\/(?:www\.)?vine\.co\/v\/([a-zA-Z0-9]{1,13}).*/);
return match && match[1].length === 11 ? match[1] : reference;
}
getVideoUrl(videoID) {
let escapedVideoID = this.env.md.utils.escapeHtml(videoID);
let escapedEmbed = this.env.md.utils.escapeHtml(this.options.embed);
return `//vine.co/v/${escapedVideoID}/embed/${escapedEmbed}`;
}
}
module.exports = VineService;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
const VideoServiceBase = require("./VideoServiceBase");
class YouTubeService extends VideoServiceBase {
getDefaultOptions() {
return { width: 640, height: 390 };
}
extractVideoID(reference) {
let match = reference.match(/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&\?]*).*/);
return match && match[7].length === 11 ? match[7] : reference;
}
getVideoUrl(videoID) {
let escapedVideoID = this.env.md.utils.escapeHtml(videoID);
return `//www.youtube.com/embed/${escapedVideoID}`;
}
}
module.exports = YouTubeService;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
const SYNTAX_CHARS = "@[]()".split("");
const SYNTAX_CODES = SYNTAX_CHARS.map(char => char.charCodeAt(0));
function advanceToSymbol(state, endLine, symbol, pointer) {
let maxPos = null;
let symbolLine = pointer.line;
let symbolIndex = state.src.indexOf(symbol, pointer.pos);
if (symbolIndex === -1) return false;
maxPos = state.eMarks[pointer.line];
while (symbolIndex >= maxPos) {
++symbolLine;
maxPos = state.eMarks[symbolLine];
if (symbolLine >= endLine) return false;
}
pointer.prevPos = pointer.pos;
pointer.pos = symbolIndex;
pointer.line = symbolLine;
return true;
}
function tokenizer(state, startLine, endLine, silent) {
let startPos = state.bMarks[startLine] + state.tShift[startLine];
let maxPos = state.eMarks[startLine];
let pointer = { line: startLine, pos: startPos };
// Block embed must be at start of input or the previous line must be blank.
if (startLine !== 0) {
let prevLineStartPos = state.bMarks[startLine - 1] + state.tShift[startLine - 1];
let prevLineMaxPos = state.eMarks[startLine - 1];
if (prevLineMaxPos > prevLineStartPos) return false;
}
// Identify as being a potential block embed.
if (maxPos - startPos < 2) return false;
if (SYNTAX_CODES[0] !== state.src.charCodeAt(pointer.pos++)) return false;
// Read service name from within square brackets.
if (SYNTAX_CODES[1] !== state.src.charCodeAt(pointer.pos++)) return false;
if (!advanceToSymbol(state, endLine, "]", pointer)) return false;
let serviceName = state.src
.substr(pointer.prevPos, pointer.pos - pointer.prevPos)
.trim()
.toLowerCase();
++pointer.pos;
// Lookup service; if unknown, then this is not a known embed!
let service = this.services[serviceName];
if (!service) return false;
// Read embed reference from within parenthesis.
if (SYNTAX_CODES[3] !== state.src.charCodeAt(pointer.pos++)) return false;
if (!advanceToSymbol(state, endLine, ")", pointer)) return false;
let videoReference = state.src
.substr(pointer.prevPos, pointer.pos - pointer.prevPos)
.trim();
++pointer.pos;
// Do not recognize as block element when there is trailing text.
maxPos = state.eMarks[pointer.line];
let trailingText = state.src
.substr(pointer.pos, maxPos - pointer.pos)
.trim();
if (trailingText !== "") return false;
// Block embed must be at end of input or the next line must be blank.
if (endLine !== pointer.line + 1) {
let nextLineStartPos = state.bMarks[pointer.line + 1] + state.tShift[pointer.line + 1];
let nextLineMaxPos = state.eMarks[pointer.line + 1];
if (nextLineMaxPos > nextLineStartPos) return false;
}
if (pointer.line >= endLine) return false;
if (!silent) {
let token = state.push("video", "div", 0);
token.markup = state.src.slice(startPos, pointer.pos);
token.block = true;
token.info = {
serviceName: serviceName,
service: service,
videoReference: videoReference,
videoID: service.extractVideoID(videoReference)
};
token.map = [ startLine, pointer.line + 1 ];
state.line = pointer.line + 1;
}
return true;
}
module.exports = tokenizer;
'use strict';
module.exports = function dimmed_plugin(md) {
// Insert each marker as a separate text token, and add it to delimiter list
function tokenize(state, silent) {
var i, scanned, token, len, ch,
start = state.pos,
marker = state.src.charCodeAt(start);
if (silent) {
return false;
}
if (marker !== 0x25/* % */) {
return false;
}
scanned = state.scanDelims(state.pos, true);
len = scanned.length;
ch = String.fromCharCode(marker);
if (len < 2) {
return false;
}
if (len % 2) {
token = state.push('text', '', 0);
token.content = ch;
len--;
}
for (i = 0; i < len; i += 2) {
token = state.push('text', '', 0);
token.content = ch + ch;
state.delimiters.push({
marker: marker,
jump: i,
token: state.tokens.length - 1,
level: state.level,
end: -1,
open: scanned.can_open,
close: scanned.can_close
});
}
state.pos += scanned.length;
return true;
}
// Walk through delimiter list and replace text tokens with tags
//
function postProcess(state) {
var i, j,
startDelim,
endDelim,
token,
loneMarkers = [],
delimiters = state.delimiters,
max = state.delimiters.length;
for (i = 0; i < max; i++) {
startDelim = delimiters[i];
if (startDelim.marker !== 0x25/* % */) {
continue;
}
if (startDelim.end === -1) {
continue;
}
endDelim = delimiters[startDelim.end];
token = state.tokens[startDelim.token];
token.type = 'dimmed_open';
token.tag = 'span';
token.attrs = [['class', 'dimmed']];
token.nesting = 1;
token.markup = '%%';
token.content = '';
token = state.tokens[endDelim.token];
token.type = 'dimmed_close';
token.tag = 'span';
token.nesting = -1;
token.markup = '%%';
token.content = '';
if (state.tokens[endDelim.token - 1].type === 'text' &&
state.tokens[endDelim.token - 1].content === '%') {
loneMarkers.push(endDelim.token - 1);
}
}
// If a marker sequence has an odd number of characters, it's splitted
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
// start of the sequence.
//
// So, we have to move all those markers after subsequent s_close tags.
//
while (loneMarkers.length) {
i = loneMarkers.pop();
j = i + 1;
while (j < state.tokens.length && state.tokens[j].type === 'dimmed_close') {
j++;
}
j--;
if (i !== j) {
token = state.tokens[j];
state.tokens[j] = state.tokens[i];
state.tokens[i] = token;
}
}
}
md.inline.ruler.before('emphasis', 'dimmed', tokenize);
md.inline.ruler2.before('emphasis', 'dimmed', postProcess);
};
/**
* Modified from https://github.com/revin/markdown-it-task-lists/blob/master/index.js, 3.0.1
*/
/*!https://github.com//markdown-it/markdown-it-footnote @license MIT */(function (f) { if (typeof exports === 'object' && typeof module !== 'undefined') { module.exports = f(); } else if (typeof define === 'function' && define.amd) { define([], f); } else { let g; if (typeof window !== 'undefined') { g = window; } else if (typeof global !== 'undefined') { g = global; } else if (typeof self !== 'undefined') { g = self; } else { g = this; }g.markdownitFootnote = f(); } }(() => {
let define; let module; let exports; return (function e(t, n, r) { function s(o, u) { if (!n[o]) { if (!t[o]) { const a = typeof require === 'function' && require; if (!u && a) return a(o, !0); if (i) return i(o, !0); const f = new Error(`Cannot find module '${o}'`); throw f.code = 'MODULE_NOT_FOUND', f; } const l = n[o] = { exports: {} }; t[o][0].call(l.exports, (e) => { const n = t[o][1][e]; return s(n || e); }, l, l.exports, e, t, n, r); } return n[o].exports; } var i = typeof require === 'function' && require; for (let o = 0; o < r.length; o++)s(r[o]); return s; }({
1: [
function (require, module, exports) {
// Process footnotes
//
// //////////////////////////////////////////////////////////////////////////////
// Renderer partials
function render_footnote_anchor_name(tokens, idx, options, env/* , slf */) {
const n = Number(tokens[idx].meta.id + 1).toString();
let prefix = '';
if (typeof env.docId === 'string') {
prefix = `-${env.docId}-`;
}
return prefix + n;
}
function render_footnote_caption(tokens, idx/* , options, env, slf */) {
let n = Number(tokens[idx].meta.id + 1).toString();
return `[${n}]`;
}
function render_footnote_ref(tokens, idx, options, env, slf) {
const id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
const caption = slf.rules.footnote_caption(tokens, idx, options, env, slf);
let refid = id;
if (tokens[idx].meta.subId > 0) {
refid += `:${tokens[idx].meta.subId}`;
}
return `<trigger for="pop:footnote${id}"><sup class="footnote-ref"><a aria-describedby="footnote-label" href="#footnote${id}" id="footnoteref${refid}">${caption}</a></sup></trigger>`;
}
function render_footnote_block_open(tokens, idx, options) {
return `${options.xhtmlOut ? '<hr class="footnotes-sep" />\n' : '<hr class="footnotes-sep">\n'
}<section class="footnotes">\n`
+ '<ol class="footnotes-list">\n';
}
function render_footnote_block_close() {
return '</ol>\n</section>\n';
}
function render_footnote_open(tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
if (tokens[idx].meta.subId > 0) {
id += `:${tokens[idx].meta.subId}`;
}
return `<li id="footnote${id}" class="footnote-item">`;
}
function render_footnote_close() {
return '</li>\n';
}
function render_footnote_anchor(tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
if (tokens[idx].meta.subId > 0) {
id += `:${tokens[idx].meta.subId}`;
}
return '';
// Below line adds backreferences, but doesn't work well with panels, so disabled for now.
/* ↩ with escape code to prevent display as Apple Emoji on iOS */
// return ` <a aria-label='Back to content' href='#footnoteref${id}' class='footnote-backref'>[${id}]</a>`;
}
module.exports = function footnote_plugin(md) {
const { parseLinkLabel } = md.helpers;
const { isSpace } = md.utils;
md.renderer.rules.footnote_ref = render_footnote_ref;
md.renderer.rules.footnote_block_open = render_footnote_block_open;
md.renderer.rules.footnote_block_close = render_footnote_block_close;
md.renderer.rules.footnote_open = render_footnote_open;
md.renderer.rules.footnote_close = render_footnote_close;
md.renderer.rules.footnote_anchor = render_footnote_anchor;
// helpers (only used in other rules, no tokens are attached to those)
md.renderer.rules.footnote_caption = render_footnote_caption;
md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name;
// Process footnote block definition
function footnote_def(state, startLine, endLine, silent) {
let oldBMark; let oldTShift; let oldSCount; let oldParentType; let pos; let label; let token;
let initial; let offset; let ch; let posAfterColon;
const start = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
// line should be at least 5 chars - "[^x]:"
if (start + 4 > max) { return false; }
if (state.src.charCodeAt(start) !== 0x5B/* [ */) { return false; }
if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) { return false; }
for (pos = start + 2; pos < max; pos++) {
if (state.src.charCodeAt(pos) === 0x20) { return false; }
if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
break;
}
}
if (pos === start + 2) { return false; } // no empty footnote labels
if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 0x3A /* : */) { return false; }
if (silent) { return true; }
pos++;
if (!state.env.footnotes) { state.env.footnotes = {}; }
if (!state.env.footnotes.refs) { state.env.footnotes.refs = {}; }
label = state.src.slice(start + 2, pos - 2);
state.env.footnotes.refs[`:${label}`] = -1;
token = new state.Token('footnote_reference_open', '', 1);
token.meta = { label };
token.level = state.level++;
state.tokens.push(token);
oldBMark = state.bMarks[startLine];
oldTShift = state.tShift[startLine];
oldSCount = state.sCount[startLine];
oldParentType = state.parentType;
posAfterColon = pos;
initial = offset = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]);
while (pos < max) {
ch = state.src.charCodeAt(pos);
if (isSpace(ch)) {
if (ch === 0x09) {
offset += 4 - offset % 4;
} else {
offset++;
}
} else {
break;
}
pos++;
}
state.tShift[startLine] = pos - posAfterColon;
state.sCount[startLine] = offset - initial;
state.bMarks[startLine] = posAfterColon;
state.blkIndent += 4;
state.parentType = 'footnote';
if (state.sCount[startLine] < state.blkIndent) {
state.sCount[startLine] += state.blkIndent;
}
state.md.block.tokenize(state, startLine, endLine, true);
state.parentType = oldParentType;
state.blkIndent -= 4;
state.tShift[startLine] = oldTShift;
state.sCount[startLine] = oldSCount;
state.bMarks[startLine] = oldBMark;
token = new state.Token('footnote_reference_close', '', -1);
token.level = --state.level;
state.tokens.push(token);
return true;
}
// Process inline footnotes (^[...])
function footnote_inline(state, silent) {
let labelStart;
let labelEnd;
let footnoteId;
let token;
let tokens;
const max = state.posMax;
const start = state.pos;
if (start + 2 >= max) { return false; }
if (state.src.charCodeAt(start) !== 0x5E/* ^ */) { return false; }
if (state.src.charCodeAt(start + 1) !== 0x5B/* [ */) { return false; }
labelStart = start + 2;
labelEnd = parseLinkLabel(state, start + 1);
// parser failed to find ']', so it's not a valid note
if (labelEnd < 0) { return false; }
// We found the end of the link, and know for a fact it's a valid link;
// so all that's left to do is to call tokenizer.
//
if (!silent) {
if (!state.env.footnotes) { state.env.footnotes = {}; }
if (!state.env.footnotes.list) { state.env.footnotes.list = []; }
footnoteId = state.env.footnotes.list.length;
state.md.inline.parse(
state.src.slice(labelStart, labelEnd),
state.md,
state.env,
tokens = [],
);
token = state.push('footnote_ref', '', 0);
token.meta = { id: footnoteId };
state.env.footnotes.list[footnoteId] = { tokens };
}
state.pos = labelEnd + 1;
state.posMax = max;
return true;
}
// Process footnote references ([^...])
function footnote_ref(state, silent) {
let label;
let pos;
let footnoteId;
let footnoteSubId;
let token;
const max = state.posMax;
const start = state.pos;
// should be at least 4 chars - "[^x]"
if (start + 3 > max) { return false; }
if (!state.env.footnotes || !state.env.footnotes.refs) { return false; }
if (state.src.charCodeAt(start) !== 0x5B/* [ */) { return false; }
if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) { return false; }
for (pos = start + 2; pos < max; pos++) {
if (state.src.charCodeAt(pos) === 0x20) { return false; }
if (state.src.charCodeAt(pos) === 0x0A) { return false; }
if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
break;
}
}
if (pos === start + 2) { return false; } // no empty footnote labels
if (pos >= max) { return false; }
pos++;
label = state.src.slice(start + 2, pos - 1);
if (typeof state.env.footnotes.refs[`:${label}`] === 'undefined') { return false; }
if (!silent) {
if (!state.env.footnotes.list) { state.env.footnotes.list = []; }
if (state.env.footnotes.refs[`:${label}`] < 0) {
footnoteId = state.env.footnotes.list.length;
state.env.footnotes.list[footnoteId] = { label, count: 0 };
state.env.footnotes.refs[`:${label}`] = footnoteId;
} else {
footnoteId = state.env.footnotes.refs[`:${label}`];
}
footnoteSubId = state.env.footnotes.list[footnoteId].count;
state.env.footnotes.list[footnoteId].count++;
token = state.push('footnote_ref', '', 0);
token.meta = { id: footnoteId, subId: footnoteSubId, label };
}
state.pos = pos;
state.posMax = max;
return true;
}
// Glue footnote tokens to end of token stream
function footnote_tail(state) {
let i; let l; let j; let t; let lastParagraph; let list; let token; let tokens; let current; let currentLabel;
let insideRef = false;
const refTokens = {};
if (!state.env.footnotes) { return; }
state.tokens = state.tokens.filter((tok) => {
//console.log("(" + JSON.stringify(tok) + ");")
if (tok.type === 'footnote_reference_open') {
insideRef = true;
current = [];
currentLabel = tok.meta.label;
return false;
}
if (tok.type === 'footnote_reference_close') {
insideRef = false;
// prepend ':' to avoid conflict with Object.prototype members
refTokens[`:${currentLabel}`] = current;
return false;
}
if (insideRef) { current.push(tok); }
return !insideRef;
});
if (!state.env.footnotes.list) { return; }
list = state.env.footnotes.list;
token = new state.Token('footnote_block_open', '', 1);
state.tokens.push(token);
for (i = 0, l = list.length; i < l; i++) {
token = new state.Token('footnote_open', '', 1);
token.meta = { id: i, label: list[i].label };
state.tokens.push(token);
if (list[i].tokens) {
tokens = [];
token = new state.Token('paragraph_open', 'p', 1);
token.block = true;
tokens.push(token);
token = new state.Token('inline', '', 0);
token.children = list[i].tokens;
token.content = '';
tokens.push(token);
token = new state.Token('paragraph_close', 'p', -1);
token.block = true;
tokens.push(token);
} else if (list[i].label) {
tokens = refTokens[`:${list[i].label}`];
}
state.tokens = state.tokens.concat(tokens);
if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') {
lastParagraph = state.tokens.pop();
} else {
lastParagraph = null;
}
t = list[i].count > 0 ? list[i].count : 1;
for (j = 0; j < t; j++) {
token = new state.Token('footnote_anchor', '', 0);
token.meta = { id: i, subId: j, label: list[i].label };
state.tokens.push(token);
}
if (lastParagraph) {
state.tokens.push(lastParagraph);
}
token = new state.Token('footnote_close', '', -1);
state.tokens.push(token);
}
token = new state.Token('footnote_block_close', '', -1);
state.tokens.push(token);
}
md.block.ruler.before('reference', 'footnote_def', footnote_def, { alt: ['paragraph', 'reference'] });
md.inline.ruler.after('image', 'footnote_inline', footnote_inline);
md.inline.ruler.after('footnote_inline', 'footnote_ref', footnote_ref);
md.core.ruler.after('inline', 'footnote_tail', footnote_tail);
};
}, {},
],
}, {}, [1]))(1);
}));
module.exports = require('markdown-it-regexp')(
/:(fa[brs]|glyphicon)-([a-z-]+):/,
(match, utils) => {
let iconFontType = match[1];
let iconFontName = match[2];
if (iconFontType === 'glyphicon') {
return `<span aria-hidden="true" class="glyphicon glyphicon-${iconFontName}"></span>`;
} else { // If icon is a Font Awesome icon
return `<span aria-hidden="true" class="${iconFontType} fa-${iconFontName}"></span>`;
}
}
);
const crypto = require('crypto');
var disableRadio = false;
var useLabelWrapper = true;
/**
* Modified from https://github.com/revin/markdown-it-task-lists/blob/master/index.js
*/
module.exports = function(md, options) {
if (options) {
disableRadio = !options.enabled;
useLabelWrapper = !!options.label;
}
md.core.ruler.after('inline', 'radio-lists', function(state) {
var tokens = state.tokens;
for (var i = 2; i < tokens.length; i++) {
if (isTodoItem(tokens, i)) {
var group = attrGet(tokens[parentToken(tokens, i-2)], 'radio-group'); // try retrieve the group id
if (group) {
group = group[1];
} else {
group = crypto.createHash('md5')
.update(tokens[i-5].content)
.update(tokens[i-4].content)
.update(tokens[i].content).digest('hex').substr(2, 5); // generate a deterministic group id
}
radioify(tokens[i], state.Token, group);
attrSet(tokens[i-2], 'class', 'radio-list-item');
attrSet(tokens[parentToken(tokens, i-2)], 'radio-group', group); // save the group id to the top-level list
attrSet(tokens[parentToken(tokens, i-2)], 'class', 'radio-list');
}
}
});
};
function attrSet(token, name, value) {
var index = token.attrIndex(name);
var attr = [name, value];
if (index < 0) {
token.attrPush(attr);
} else {
token.attrs[index] = attr;
}
}
function attrGet(token, name) {
var index = token.attrIndex(name);
if (index < 0) {
return void(0);
} else {
return token.attrs[index];
}
}
function parentToken(tokens, index) {
var targetLevel = tokens[index].level - 1;
for (var i = index - 1; i >= 0; i--) {
if (tokens[i].level === targetLevel) {
return i;
}
}
return -1;
}
function isTodoItem(tokens, index) {
return isInline(tokens[index]) &&
isParagraph(tokens[index - 1]) &&
isListItem(tokens[index - 2]) &&
startsWithTodoMarkdown(tokens[index]);
}
function radioify(token, TokenConstructor, radioId) {
token.children.unshift(makeRadioButton(token, TokenConstructor, radioId));
token.children[1].content = token.children[1].content.slice(3);
token.content = token.content.slice(3);
if (useLabelWrapper) {
token.children.unshift(beginLabel(TokenConstructor));
token.children.push(endLabel(TokenConstructor));
}
}
function makeRadioButton(token, TokenConstructor, radioId) {
var radio = new TokenConstructor('html_inline', '', 0);
var disabledAttr = disableRadio ? ' disabled="" ' : '';
if (token.content.indexOf('( ) ') === 0) {
radio.content = '<input class="radio-list-input" name="' + radioId + '"' + disabledAttr + 'type="radio">';
} else if (token.content.indexOf('(x) ') === 0 || token.content.indexOf('(X) ') === 0) {
radio.content = '<input class="radio-list-input" checked="" name="' + radioId + '"' + disabledAttr + 'type="radio">';
}
return radio;
}
// these next two functions are kind of hacky; probably should really be a
// true block-level token with .tag=='label'
function beginLabel(TokenConstructor) {
var token = new TokenConstructor('html_inline', '', 0);
token.content = '<label>';
return token;
}
function endLabel(TokenConstructor) {
var token = new TokenConstructor('html_inline', '', 0);
token.content = '</label>';
return token;
}
function isInline(token) { return token.type === 'inline'; }
function isParagraph(token) { return token.type === 'paragraph_open'; }
function isListItem(token) { return token.type === 'list_item_open'; }
function startsWithTodoMarkdown(token) {
// leading whitespace in a list item is already trimmed off by markdown-it
return token.content.indexOf('( ) ') === 0 || token.content.indexOf('(x) ') === 0 || token.content.indexOf('(X) ') === 0;
}
let mdurl = require('mdurl');
let punycode = require('punycode');
let RECODE_HOSTNAME_FOR = [ 'http:', 'https:', 'mailto:' ];
mdurl.encode.defaultChars = mdurl.encode.defaultChars += '{}';
function normalizeLink(url) {
var parsed = mdurl.parse(url, true);
if (parsed.hostname) {
// Encode hostnames in urls like:
// `http://host/`, `https://host/`, `mailto:user@host`, `//host/`
//
// We don't encode unknown schemas, because it's likely that we encode
// something we shouldn't (e.g. `skype:name` treated as `skype:host`)
//
if (!parsed.protocol || RECODE_HOSTNAME_FOR.indexOf(parsed.protocol) >= 0) {
try {
parsed.hostname = punycode.toASCII(parsed.hostname);
} catch (er) { /**/ }
}
}
return mdurl.encode(mdurl.format(parsed));
}
module.exports = normalizeLink;
const cheerio = require('cheerio');
const fs = require('fs');
const htmlparser = require('htmlparser2'); require('./patches/htmlparser2');
const nunjucks = require('nunjucks');
const path = require('path');
const pathIsInside = require('path-is-inside');
const Promise = require('bluebird');
const url = require('url');
const _ = {};
_.clone = require('lodash/clone');
_.cloneDeep = require('lodash/cloneDeep');
_.hasIn = require('lodash/hasIn');
_.isArray = require('lodash/isArray');
_.isEmpty = require('lodash/isEmpty');
_.pick = require('lodash/pick');
const CyclicReferenceError = require('./handlers/cyclicReferenceError.js');
const md = require('./lib/markdown-it');
const utils = require('./utils');
cheerio.prototype.options.xmlMode = true; // Enable xml mode for self-closing tag
cheerio.prototype.options.decodeEntities = false; // Don't escape HTML entities
const {
ATTRIB_INCLUDE_PATH,
ATTRIB_CWF,
BOILERPLATE_FOLDER_NAME,
IMPORTED_VARIABLE_PREFIX,
} = require('./constants');
const VARIABLE_LOOKUP = new Map();
const FILE_ALIASES = new Map();
const PROCESSED_INNER_VARIABLES = new Set();
/*
* Utils
*/
/**
* @throws Will throw an error if a non-absolute path or path outside the root is given
*/
function calculateNewBaseUrls(filePath, root, lookUp) {
if (!path.isAbsolute(filePath)) {
throw new Error(`Non-absolute path given to calculateNewBaseUrls: '${filePath}'`);
}
if (!pathIsInside(filePath, root)) {
throw new Error(`Path given '${filePath}' is not in root '${root}'`);
}
function calculate(file, result) {
if (file === root) {
return { relative: path.relative(root, root), parent: root };
}
const parent = path.dirname(file);
if (lookUp.has(parent) && result.length === 1) {
return { relative: path.relative(parent, result[0]), parent };
} else if (lookUp.has(parent)) {
return calculate(parent, [parent]);
}
return calculate(parent, result);
}
return calculate(filePath, []);
}
function calculateBoilerplateFilePath(pathInBoilerplates, asIfAt, config) {
const { parent, relative } = calculateNewBaseUrls(asIfAt, config.rootPath, config.baseUrlMap);
return path.resolve(parent, relative, BOILERPLATE_FOLDER_NAME, pathInBoilerplates);
}
function createErrorNode(element, error) {
const errorElement = cheerio.parseHTML(utils.createErrorElement(error), true)[0];
return Object.assign(element, _.pick(errorElement, ['name', 'attribs', 'children']));
}
function createEmptyNode() {
const emptyElement = cheerio.parseHTML('<div></div>', true)[0];
return emptyElement;
}
function isText(element) {
return element.type === 'text' || element.type === 'comment';
}
function Parser(options) {
this._options = options || {};
// eslint-disable-next-line no-console
this._onError = this._options.errorHandler || console.error;
this._fileCache = {};
this.dynamicIncludeSrc = [];
this.staticIncludeSrc = [];
this.boilerplateIncludeSrc = [];
this.missingIncludeSrc = [];
}
Parser.resetVariables = function () {
VARIABLE_LOOKUP.clear();
FILE_ALIASES.clear();
PROCESSED_INNER_VARIABLES.clear();
};
/**
* Extract variables from an include element
* @param includeElement include element to extract variables from
* @param contextVariables variables defined by parent pages
*/
function extractIncludeVariables(includeElement, contextVariables) {
const includedVariables = { ...contextVariables };
Object.keys(includeElement.attribs).forEach((attribute) => {
if (!attribute.startsWith('var-')) {
return;
}
const variableName = attribute.replace(/^var-/, '');
if (!includedVariables[variableName]) {
includedVariables[variableName] = includeElement.attribs[attribute];
}
});
if (includeElement.children) {
includeElement.children.forEach((child) => {
if (child.name !== 'variable' && child.name !== 'span') {
return;
}
const variableName = child.attribs.name || child.attribs.id;
if (!variableName) {
// eslint-disable-next-line no-console
console.warn(`Missing reference in ${includeElement.attribs[ATTRIB_CWF]}\n`
+ `Missing 'name' or 'id' in variable for ${includeElement.attribs.src} include.`);
return;
}
if (!includedVariables[variableName]) {
includedVariables[variableName] = cheerio.html(child.children);
}
});
}
return includedVariables;
}
/**
* Returns an object containing the imported variables for specified file
* @param file file name to get the imported variables for
*/
function getImportedVariableMap(file) {
const innerVariables = {};
FILE_ALIASES.get(file).forEach((actualPath, alias) => {
innerVariables[alias] = {};
const variables = VARIABLE_LOOKUP.get(actualPath);
variables.forEach((value, name) => {
innerVariables[alias][name] = value;
});
});
return innerVariables;
}
/**
* Extract page variables from a page
* @param filename for error printing
* @param data to extract variables from
* @param userDefinedVariables global variables
* @param includedVariables variables from parent include
*/
function extractPageVariables(fileName, data, userDefinedVariables, includedVariables) {
const $ = cheerio.load(data);
const pageVariables = { };
VARIABLE_LOOKUP.set(fileName, new Map());
/**
* <import>ed variables have not been processed yet, we replace such variables with itself first.
*/
const importedVariables = {};
$('import[from]').each((index, element) => {
const variableNames = Object.keys(element.attribs)
.filter(name => name !== 'from' && name !== 'as');
// If no namespace is provided, we use the smallest name as one...
const largestName = variableNames.sort()[0];
// ... and prepend it with $__MARKBIND__ to reduce collisions.
const generatedAlias = IMPORTED_VARIABLE_PREFIX + largestName;
const hasAlias = _.hasIn(element.attribs, 'as');
const alias = hasAlias ? element.attribs.as : generatedAlias;
importedVariables[alias] = new Proxy({}, {
get(obj, prop) {
return `{{${alias}.${prop}}}`;
},
});
variableNames.forEach((name) => {
importedVariables[name] = `{{${alias}.${name}}}`;
});
});
$('variable').each(function () {
const variableElement = $(this);
const variableName = variableElement.attr('name');
if (!variableName) {
// eslint-disable-next-line no-console
console.warn(`Missing 'name' for variable in ${fileName}\n`);
return;
}
if (!pageVariables[variableName]) {
const variableValue
= nunjucks.renderString(
md.renderInline(variableElement.html()),
{
...importedVariables, ...pageVariables, ...userDefinedVariables, ...includedVariables,
},
);
pageVariables[variableName] = variableValue;
VARIABLE_LOOKUP.get(fileName).set(variableName, variableValue);
}
});
return { ...importedVariables, ...pageVariables };
}
Parser.prototype.getDynamicIncludeSrc = function () {
return _.clone(this.dynamicIncludeSrc);
};
Parser.prototype.getStaticIncludeSrc = function () {
return _.clone(this.staticIncludeSrc);
};
Parser.prototype.getBoilerplateIncludeSrc = function () {
return _.clone(this.boilerplateIncludeSrc);
};
Parser.prototype.getMissingIncludeSrc = function () {
return _.clone(this.missingIncludeSrc);
};
Parser.prototype._preprocessThumbnails = function (element) {
const isImage = _.hasIn(element.attribs, 'src') && element.attribs.src !== '';
if (isImage) {
return element;
}
const text = _.hasIn(element.attribs, 'text') ? element.attribs.text : '';
if (text === '') {
return element;
}
const renderedText = md.renderInline(text);
// eslint-disable-next-line no-param-reassign
element.children = cheerio.parseHTML(renderedText);
return element;
};
Parser.prototype._renderIncludeFile = function (filePath, element, context, config, asIfAt = filePath) {
try {
this._fileCache[filePath] = this._fileCache[filePath]
? this._fileCache[filePath] : fs.readFileSync(filePath, 'utf8');
} catch (e) {
// Read file fail
const missingReferenceErrorMessage = `Missing reference in: ${element.attribs[ATTRIB_CWF]}`;
e.message += `\n${missingReferenceErrorMessage}`;
this._onError(e);
return createErrorNode(element, e);
}
const fileContent = this._fileCache[filePath]; // cache the file contents to save some I/O
const { parent, relative }
= calculateNewBaseUrls(asIfAt, config.rootPath, config.baseUrlMap);
const userDefinedVariables = config.userDefinedVariablesMap[path.resolve(parent, relative)];
// Extract included variables from the PARENT file
const includeVariables = extractIncludeVariables(element, context.variables);
// Extract page variables from the CHILD file
const pageVariables = extractPageVariables(asIfAt, fileContent,
userDefinedVariables, includeVariables);
const content = nunjucks.renderString(fileContent,
{ ...pageVariables, ...includeVariables, ...userDefinedVariables },
{ path: filePath });
const childContext = _.cloneDeep(context);
childContext.cwf = asIfAt;
childContext.variables = includeVariables;
return { content, childContext, userDefinedVariables };
};
Parser.prototype._extractInnerVariables = function (content, context, config) {
const { cwf } = context;
const $ = cheerio.load(content, {
xmlMode: false,
decodeEntities: false,
});
const aliases = new Map();
FILE_ALIASES.set(cwf, aliases);
$('import[from]').each((index, element) => {
const filePath = path.resolve(path.dirname(cwf), element.attribs.from);
const variableNames = Object.keys(element.attribs)
.filter(name => name !== 'from' && name !== 'as');
// If no namespace is provided, we use the smallest name as one
const largestName = variableNames.sort()[0];
// ... and prepend it with $__MARKBIND__ to reduce collisions.
const generatedAlias = IMPORTED_VARIABLE_PREFIX + largestName;
const alias = _.hasIn(element.attribs, 'as')
? element.attribs.as
: generatedAlias;
aliases.set(alias, filePath);
this.staticIncludeSrc.push({ from: context.cwf, to: filePath });
// Render inner file content
const { content: renderedContent, childContext, userDefinedVariables }
= this._renderIncludeFile(filePath, element, context, config);
if (!PROCESSED_INNER_VARIABLES.has(filePath)) {
PROCESSED_INNER_VARIABLES.add(filePath);
this._extractInnerVariables(renderedContent, childContext, config);
}
const innerVariables = getImportedVariableMap(filePath);
VARIABLE_LOOKUP.get(filePath).forEach((value, variableName, map) => {
map.set(variableName, nunjucks.renderString(value, { ...userDefinedVariables, ...innerVariables }));
});
});
};
Parser.prototype._preprocess = function (node, context, config) {
const element = node;
const self = this;
element.attribs = element.attribs || {};
element.attribs[ATTRIB_CWF] = path.resolve(context.cwf);
if (element.name === 'thumbnail') {
return this._preprocessThumbnails(element);
}
const requiresSrc = ['include'].includes(element.name);
if (requiresSrc && _.isEmpty(element.attribs.src)) {
const error = new Error(`Empty src attribute in ${element.name} in: ${element.attribs[ATTRIB_CWF]}`);
this._onError(error);
return createErrorNode(element, error);
}
const shouldProcessSrc = ['include', 'panel'].includes(element.name);
const hasSrc = _.hasIn(element.attribs, 'src');
let isUrl;
let includeSrc;
let filePath;
let actualFilePath;
if (hasSrc && shouldProcessSrc) {
isUrl = utils.isUrl(element.attribs.src);
includeSrc = url.parse(element.attribs.src);
filePath = isUrl
? element.attribs.src
: path.resolve(path.dirname(context.cwf), decodeURIComponent(includeSrc.path));
actualFilePath = filePath;
const isBoilerplate = _.hasIn(element.attribs, 'boilerplate');
if (isBoilerplate) {
element.attribs.boilerplate = element.attribs.boilerplate || path.basename(filePath);
actualFilePath = calculateBoilerplateFilePath(element.attribs.boilerplate, filePath, config);
this.boilerplateIncludeSrc.push({ from: context.cwf, to: actualFilePath });
}
const isOptional = element.name === 'include' && _.hasIn(element.attribs, 'optional');
if (!utils.fileExists(actualFilePath)) {
if (isOptional) {
return createEmptyNode();
}
this.missingIncludeSrc.push({ from: context.cwf, to: actualFilePath });
const error = new Error(
`No such file: ${actualFilePath}\nMissing reference in ${element.attribs[ATTRIB_CWF]}`,
);
this._onError(error);
return createErrorNode(element, error);
}
}
if (element.name === 'include') {
const isInline = _.hasIn(element.attribs, 'inline');
const isDynamic = _.hasIn(element.attribs, 'dynamic');
const isOptional = _.hasIn(element.attribs, 'optional');
const isTrim = _.hasIn(element.attribs, 'trim');
element.name = isInline ? 'span' : 'div';
element.attribs[ATTRIB_INCLUDE_PATH] = filePath;
if (isOptional && !includeSrc.hash) {
// optional includes of whole files have been handled, but segments still need to be processed
delete element.attribs.optional;
}
if (isDynamic) {
element.name = 'panel';
element.attribs.src = filePath;
element.attribs['no-close'] = true;
element.attribs['no-switch'] = true;
if (includeSrc.hash) {
element.attribs.fragment = includeSrc.hash.substring(1);
}
element.attribs.header = element.attribs.name || '';
delete element.attribs.dynamic;
this.dynamicIncludeSrc.push({ from: context.cwf, to: actualFilePath, asIfTo: element.attribs.src });
return element;
}
if (isUrl) {
return element; // only keep url path for dynamic
}
this.staticIncludeSrc.push({ from: context.cwf, to: actualFilePath });
const isIncludeSrcMd = utils.isMarkdownFileExt(utils.getExt(filePath));
if (isIncludeSrcMd && context.source === 'html') {
// HTML include markdown, use special tag to indicate markdown code.
element.name = 'markdown';
}
const { content, childContext, userDefinedVariables }
= this._renderIncludeFile(actualFilePath, element, context, config, filePath);
childContext.source = isIncludeSrcMd ? 'md' : 'html';
childContext.callStack.push(context.cwf);
if (!PROCESSED_INNER_VARIABLES.has(filePath)) {
PROCESSED_INNER_VARIABLES.add(filePath);
this._extractInnerVariables(content, childContext, config);
}
const innerVariables = getImportedVariableMap(filePath);
const fileContent = nunjucks.renderString(content, { ...userDefinedVariables, ...innerVariables });
// Delete variable attributes in include
Object.keys(element.attribs).forEach((attribute) => {
if (attribute.startsWith('var-')) {
delete element.attribs[attribute];
}
});
delete element.attribs.boilerplate;
delete element.attribs.src;
delete element.attribs.inline;
delete element.attribs.trim;
if (includeSrc.hash) {
// directly get segment from the src
const segmentSrc = cheerio.parseHTML(fileContent, true);
const $ = cheerio.load(segmentSrc);
const hashContent = $(includeSrc.hash).html();
let actualContent = (hashContent && isTrim) ? hashContent.trim() : hashContent;
if (actualContent === null) {
if (isOptional) {
// set empty content for optional segment include that does not exist
actualContent = '';
} else {
const error = new Error(
`No such segment '${includeSrc.hash.substring(1)}' in file: ${actualFilePath}`
+ `\nMissing reference in ${element.attribs[ATTRIB_CWF]}`,
);
this._onError(error);
return createErrorNode(element, error);
}
}
if (isOptional) {
// optional includes of segments have now been handled, so delete the attribute
delete element.attribs.optional;
}
if (isIncludeSrcMd) {
if (context.mode === 'include') {
actualContent = isInline ? actualContent : utils.wrapContent(actualContent, '\n\n', '\n');
} else {
actualContent = md.render(actualContent);
}
actualContent = self._rebaseReferenceForStaticIncludes(actualContent, element, config);
}
const wrapperType = isInline ? 'span' : 'div';
element.children = cheerio.parseHTML(
`<${wrapperType} data-included-from="${filePath}">${actualContent}</${wrapperType}>`,
true,
);
} else {
let actualContent = (fileContent && isTrim) ? fileContent.trim() : fileContent;
if (isIncludeSrcMd) {
if (context.mode === 'include') {
actualContent = isInline ? actualContent : utils.wrapContent(actualContent, '\n\n', '\n');
} else {
actualContent = md.render(actualContent);
}
}
const wrapperType = isInline ? 'span' : 'div';
element.children = cheerio.parseHTML(
`<${wrapperType} data-included-from="${filePath}">${actualContent}</${wrapperType}>`,
true,
);
}
if (element.children && element.children.length > 0) {
if (childContext.callStack.length > CyclicReferenceError.MAX_RECURSIVE_DEPTH) {
const error = new CyclicReferenceError(childContext.callStack);
this._onError(error);
return createErrorNode(element, error);
}
element.children = element.children.map(e => self._preprocess(e, childContext, config));
}
} else if ((element.name === 'panel') && hasSrc) {
if (!isUrl && includeSrc.hash) {
element.attribs.fragment = includeSrc.hash.substring(1); // save hash to fragment attribute
}
element.attribs.src = filePath;
this.dynamicIncludeSrc.push({ from: context.cwf, to: actualFilePath, asIfTo: filePath });
return element;
} else if (element.name === 'variable' || element.name === 'import') {
return createEmptyNode();
} else {
if (element.name === 'body') {
// eslint-disable-next-line no-console
console.warn(`<body> tag found in ${element.attribs[ATTRIB_CWF]}. This may cause formatting errors.`);
}
if (element.children && element.children.length > 0) {
element.children = element.children.map(e => self._preprocess(e, context, config));
}
}
return element;
};
Parser.prototype.processDynamicResources = function (context, html) {
const self = this;
const $ = cheerio.load(html, {
xmlMode: false,
decodeEntities: false,
});
$('img, pic, thumbnail').each(function () {
const elem = $(this);
if (elem[0].name === 'thumbnail' && elem.attr('src') === undefined) {
// Thumbnail tag without src
return;
}
const resourcePath = utils.ensurePosix(elem.attr('src'));
if (resourcePath === undefined || resourcePath === '') {
// Found empty img/pic resource in resourcePath
return;
}
if (utils.isAbsolutePath(resourcePath) || utils.isUrl(resourcePath)) {
// Do not rewrite.
return;
}
const firstParent = elem.closest('div[data-included-from], span[data-included-from]');
const originalSrc = utils.ensurePosix(firstParent.attr('data-included-from') || context);
const originalSrcFolder = path.posix.dirname(originalSrc);
const fullResourcePath = path.posix.join(originalSrcFolder, resourcePath);
const resolvedResourcePath = path.posix.relative(utils.ensurePosix(self.rootPath), fullResourcePath);
const absoluteResourcePath = path.posix.join('{{hostBaseUrl}}', resolvedResourcePath);
$(this).attr('src', absoluteResourcePath);
});
$('a, link').each(function () {
const elem = $(this);
const resourcePath = elem.attr('href');
if (resourcePath === undefined || resourcePath === '') {
// Found empty href resource in resourcePath
return;
}
if (utils.isAbsolutePath(resourcePath) || utils.isUrl(resourcePath) || resourcePath.startsWith('#')) {
// Do not rewrite.
return;
}
const firstParent = elem.closest('div[data-included-from], span[data-included-from]');
const originalSrc = utils.ensurePosix(firstParent.attr('data-included-from') || context);
const originalSrcFolder = path.posix.dirname(originalSrc);
const fullResourcePath = path.posix.join(originalSrcFolder, resourcePath);
const resolvedResourcePath = path.posix.relative(utils.ensurePosix(self.rootPath), fullResourcePath);
const absoluteResourcePath = path.posix.join('{{hostBaseUrl}}', resolvedResourcePath);
$(this).attr('href', absoluteResourcePath);
});
return $.html();
};
Parser.prototype.unwrapIncludeSrc = function (html) {
const $ = cheerio.load(html, {
xmlMode: false,
decodeEntities: false,
});
$('div[data-included-from], span[data-included-from]').each(function () {
$(this).replaceWith($(this).contents());
});
return $.html();
};
Parser.prototype._parse = function (node, context, config) {
const element = node;
const self = this;
if (_.isArray(element)) {
return element.map(el => self._parse(el, context, config));
}
if (isText(element)) {
return element;
}
if (element.name) {
element.name = element.name.toLowerCase();
}
switch (element.name) {
case 'md':
element.name = 'span';
cheerio.prototype.options.xmlMode = false;
element.children = cheerio.parseHTML(md.renderInline(cheerio.html(element.children)), true);
cheerio.prototype.options.xmlMode = true;
break;
case 'markdown':
element.name = 'div';
cheerio.prototype.options.xmlMode = false;
element.children = cheerio.parseHTML(md.render(cheerio.html(element.children)), true);
cheerio.prototype.options.xmlMode = true;
break;
case 'panel': {
if (!_.hasIn(element.attribs, 'src')) { // dynamic panel
break;
}
const fileExists = utils.fileExists(element.attribs.src)
|| utils.fileExists(calculateBoilerplateFilePath(element.attribs.boilerplate,
element.attribs.src, config));
if (fileExists) {
const { src, fragment } = element.attribs;
const resultDir = path.dirname(path.join('{{hostBaseUrl}}', path.relative(config.rootPath, src)));
const resultPath = path.join(resultDir, utils.setExtension(path.basename(src), '._include_.html'));
element.attribs.src = utils.ensurePosix(fragment ? `${resultPath}#${fragment}` : resultPath);
}
delete element.attribs.boilerplate;
break;
}
default:
break;
}
if (element.children) {
element.children.forEach((child) => {
self._parse(child, context, config);
});
}
return element;
};
Parser.prototype._trimNodes = function (node) {
const self = this;
if (node.name === 'pre' || node.name === 'code') {
return;
}
if (node.children) {
/* eslint-disable no-plusplus */
for (let n = 0; n < node.children.length; n++) {
const child = node.children[n];
if (
child.type === 'comment'
|| (child.type === 'text' && n === node.children.length - 1 && !/\S/.test(child.data))
) {
node.children.splice(n, 1);
n--;
} else if (child.type === 'tag') {
self._trimNodes(child);
}
}
/* eslint-enable no-plusplus */
}
};
Parser.prototype.includeFile = function (file, config) {
const context = {};
context.cwf = config.cwf || file; // current working file
context.mode = 'include';
context.callStack = [];
return new Promise((resolve, reject) => {
const handler = new htmlparser.DomHandler((error, dom) => {
if (error) {
reject(error);
return;
}
const nodes = dom.map((d) => {
let processed;
try {
processed = this._preprocess(d, context, config);
} catch (err) {
err.message += `\nError while preprocessing '${file}'`;
this._onError(err);
processed = createErrorNode(d, err);
}
return processed;
});
resolve(cheerio.html(nodes));
});
const parser = new htmlparser.Parser(handler, {
xmlMode: true,
decodeEntities: true,
});
let actualFilePath = file;
if (!utils.fileExists(file)) {
const boilerplateFilePath = calculateBoilerplateFilePath(path.basename(file), file, config);
if (utils.fileExists(boilerplateFilePath)) {
actualFilePath = boilerplateFilePath;
}
}
// Read files
fs.readFile(actualFilePath, 'utf-8', (err, data) => {
if (err) {
reject(err);
return;
}
const { parent, relative } = calculateNewBaseUrls(file, config.rootPath, config.baseUrlMap);
const userDefinedVariables = config.userDefinedVariablesMap[path.resolve(parent, relative)];
const pageVariables = extractPageVariables(file, data, userDefinedVariables, {});
let fileContent = nunjucks.renderString(data,
{ ...pageVariables, ...userDefinedVariables },
{ path: actualFilePath });
this._extractInnerVariables(fileContent, context, config);
const innerVariables = getImportedVariableMap(context.cwf);
fileContent = nunjucks.renderString(fileContent, { ...userDefinedVariables, ...innerVariables });
const fileExt = utils.getExt(file);
if (utils.isMarkdownFileExt(fileExt)) {
context.source = 'md';
parser.parseComplete(fileContent.toString());
} else if (fileExt === 'html') {
context.source = 'html';
parser.parseComplete(fileContent);
} else {
const error = new Error(`Unsupported File Extension: '${fileExt}'`);
reject(error);
}
});
});
};
Parser.prototype.renderFile = function (file, config) {
const context = {};
context.cwf = file; // current working file
context.mode = 'render';
return new Promise((resolve, reject) => {
const handler = new htmlparser.DomHandler((error, dom) => {
if (error) {
reject(error);
return;
}
const nodes = dom.map((d) => {
let parsed;
try {
parsed = this._parse(d, context, config);
} catch (err) {
err.message += `\nError while rendering '${file}'`;
this._onError(err);
parsed = createErrorNode(d, err);
}
return parsed;
});
nodes.forEach((d) => {
this._trimNodes(d);
});
cheerio.prototype.options.xmlMode = false;
resolve(cheerio.html(nodes));
cheerio.prototype.options.xmlMode = true;
});
const parser = new htmlparser.Parser(handler, {
xmlMode: true,
decodeEntities: false,
});
// Read files
fs.readFile(file, (err, data) => {
if (err) {
reject(err);
return;
}
const fileExt = utils.getExt(file);
if (utils.isMarkdownFileExt(fileExt)) {
const inputData = md.render(data.toString());
context.source = 'md';
parser.parseComplete(inputData);
} else if (fileExt === 'html') {
context.source = 'html';
parser.parseComplete(data);
} else {
const error = new Error(`Unsupported File Extension: '${fileExt}'`);
reject(error);
}
});
});
};
Parser.prototype.resolveBaseUrl = function (pageData, config) {
const { baseUrlMap, rootPath, isDynamic } = config;
this.baseUrlMap = baseUrlMap;
this.rootPath = rootPath;
this.isDynamic = isDynamic || false;
if (this.isDynamic) {
this.dynamicSource = config.dynamicSource;
}
return new Promise((resolve, reject) => {
const handler = new htmlparser.DomHandler((error, dom) => {
if (error) {
reject(error);
return;
}
const nodes = dom.map((d) => {
const node = d;
const childrenBase = {};
if (this.isDynamic) {
// Change CWF for each top level element
if (node.attribs) {
node.attribs[ATTRIB_CWF] = this.dynamicSource;
}
}
return this._rebaseReference(node, childrenBase);
});
cheerio.prototype.options.xmlMode = false;
resolve(cheerio.html(nodes));
cheerio.prototype.options.xmlMode = true;
});
const parser = new htmlparser.Parser(handler, {
xmlMode: true,
decodeEntities: true,
});
parser.parseComplete(pageData);
});
};
Parser.prototype._rebaseReference = function (node, foundBase) {
const element = node;
if (_.isArray(element)) {
return element.map(el => this._rebaseReference(el, foundBase));
}
if (isText(element)) {
return element;
}
// Rebase children element
const childrenBase = {};
element.children.forEach((el) => {
this._rebaseReference(el, childrenBase);
});
// rebase current element
if (element.attribs[ATTRIB_INCLUDE_PATH]) {
const filePath = element.attribs[ATTRIB_INCLUDE_PATH];
let newBase = calculateNewBaseUrls(filePath, this.rootPath, this.baseUrlMap);
if (newBase) {
const { relative, parent } = newBase;
// eslint-disable-next-line no-param-reassign
foundBase[parent] = relative;
}
const combinedBases = { ...childrenBase, ...foundBase };
const bases = Object.keys(combinedBases);
if (bases.length !== 0) {
// need to rebase
newBase = combinedBases[bases[0]];
const { children } = element;
if (children) {
const currentBase = calculateNewBaseUrls(element.attribs[ATTRIB_CWF], this.rootPath, this.baseUrlMap);
if (currentBase) {
if (currentBase.relative !== newBase) {
cheerio.prototype.options.xmlMode = false;
const newBaseUrl = `{{hostBaseUrl}}/${newBase}`;
const rendered = nunjucks.renderString(cheerio.html(children), {
// This is to prevent the nunjuck call from converting {{hostBaseUrl}} to an empty string
// and let the hostBaseUrl value be injected later.
hostBaseUrl: '{{hostBaseUrl}}',
baseUrl: newBaseUrl,
}, { path: filePath });
element.children = cheerio.parseHTML(rendered, true);
cheerio.prototype.options.xmlMode = true;
}
}
}
}
delete element.attribs[ATTRIB_INCLUDE_PATH];
}
delete element.attribs[ATTRIB_CWF];
return element;
};
Parser.prototype._rebaseReferenceForStaticIncludes = function (pageData, element, config) {
if (!config) {
return pageData;
}
if (!pageData.includes('{{baseUrl}}')) {
return pageData;
}
const filePath = element.attribs[ATTRIB_INCLUDE_PATH];
const fileBase = calculateNewBaseUrls(filePath, config.rootPath, config.baseUrlMap);
if (!fileBase.relative) {
return pageData;
}
const currentPath = element.attribs[ATTRIB_CWF];
const currentBase = calculateNewBaseUrls(currentPath, config.rootPath, config.baseUrlMap);
if (currentBase.relative === fileBase.relative) {
return pageData;
}
const newBase = fileBase.relative;
const newBaseUrl = `{{hostBaseUrl}}/${newBase}`;
return nunjucks.renderString(pageData, { baseUrl: newBaseUrl }, { path: filePath });
};
module.exports = Parser;
const { Tokenizer } = require('htmlparser2');
const MARKDOWN = Symbol('MARKDOWN');
/* eslint-disable
brace-style,
indent,
keyword-spacing,
max-len,
no-mixed-spaces-and-tabs,
no-multi-spaces,
no-plusplus,
no-tabs,
no-unused-vars,
no-var,
one-var,
quotes,
semi,
space-before-blocks,
space-before-function-paren,
spaced-comment,
vars-on-top,
*/
var i = 0,
TEXT = i++,
BEFORE_TAG_NAME = i++, //after <
IN_TAG_NAME = i++,
IN_SELF_CLOSING_TAG = i++,
BEFORE_CLOSING_TAG_NAME = i++,
IN_CLOSING_TAG_NAME = i++,
AFTER_CLOSING_TAG_NAME = i++,
//attributes
BEFORE_ATTRIBUTE_NAME = i++,
IN_ATTRIBUTE_NAME = i++,
AFTER_ATTRIBUTE_NAME = i++,
BEFORE_ATTRIBUTE_VALUE = i++,
IN_ATTRIBUTE_VALUE_DQ = i++, // "
IN_ATTRIBUTE_VALUE_SQ = i++, // '
IN_ATTRIBUTE_VALUE_NQ = i++,
//declarations
BEFORE_DECLARATION = i++, // !
IN_DECLARATION = i++,
//processing instructions
IN_PROCESSING_INSTRUCTION = i++, // ?
//comments
BEFORE_COMMENT = i++,
IN_COMMENT = i++,
AFTER_COMMENT_1 = i++,
AFTER_COMMENT_2 = i++,
//cdata
BEFORE_CDATA_1 = i++, // [
BEFORE_CDATA_2 = i++, // C
BEFORE_CDATA_3 = i++, // D
BEFORE_CDATA_4 = i++, // A
BEFORE_CDATA_5 = i++, // T
BEFORE_CDATA_6 = i++, // A
IN_CDATA = i++, // [
AFTER_CDATA_1 = i++, // ]
AFTER_CDATA_2 = i++, // ]
//special tags
BEFORE_SPECIAL = i++, //S
BEFORE_SPECIAL_END = i++, //S
BEFORE_SCRIPT_1 = i++, //C
BEFORE_SCRIPT_2 = i++, //R
BEFORE_SCRIPT_3 = i++, //I
BEFORE_SCRIPT_4 = i++, //P
BEFORE_SCRIPT_5 = i++, //T
AFTER_SCRIPT_1 = i++, //C
AFTER_SCRIPT_2 = i++, //R
AFTER_SCRIPT_3 = i++, //I
AFTER_SCRIPT_4 = i++, //P
AFTER_SCRIPT_5 = i++, //T
BEFORE_STYLE_1 = i++, //T
BEFORE_STYLE_2 = i++, //Y
BEFORE_STYLE_3 = i++, //L
BEFORE_STYLE_4 = i++, //E
AFTER_STYLE_1 = i++, //T
AFTER_STYLE_2 = i++, //Y
AFTER_STYLE_3 = i++, //L
AFTER_STYLE_4 = i++, //E
BEFORE_ENTITY = i++, //&
BEFORE_NUMERIC_ENTITY = i++, //#
IN_NAMED_ENTITY = i++,
IN_NUMERIC_ENTITY = i++,
IN_HEX_ENTITY = i++, //X
j = 0,
SPECIAL_NONE = j++,
SPECIAL_SCRIPT = j++,
SPECIAL_STYLE = j++;
Tokenizer.prototype._stateMarkdown = function(c){
if(c === '`') {
this._state = TEXT;
}
}
Tokenizer.prototype._stateText = function(c){
if(c === '`'){
this._state = MARKDOWN;
} else if(c === "<"){
var isInequality = (this._index + 1 < this._buffer.length) && (this._buffer.charAt(this._index + 1) === '=');
if(!isInequality){
if(this._index > this._sectionStart){
this._cbs.ontext(this._getSection());
}
this._state = BEFORE_TAG_NAME;
this._sectionStart = this._index;
}
} else if(this._decodeEntities && this._special === SPECIAL_NONE && c === "&"){
if(this._index > this._sectionStart){
this._cbs.ontext(this._getSection());
}
this._baseState = TEXT;
this._state = BEFORE_ENTITY;
this._sectionStart = this._index;
}
};
Tokenizer.prototype._parse = function(){
while(this._index < this._buffer.length && this._running){
var c = this._buffer.charAt(this._index);
if(this._state === MARKDOWN){
this._stateMarkdown(c);
} else if(this._state === TEXT){
this._stateText(c);
} else if(this._state === BEFORE_TAG_NAME){
this._stateBeforeTagName(c);
} else if(this._state === IN_TAG_NAME){
this._stateInTagName(c);
} else if(this._state === BEFORE_CLOSING_TAG_NAME){
this._stateBeforeCloseingTagName(c);
} else if(this._state === IN_CLOSING_TAG_NAME){
this._stateInCloseingTagName(c);
} else if(this._state === AFTER_CLOSING_TAG_NAME){
this._stateAfterCloseingTagName(c);
} else if(this._state === IN_SELF_CLOSING_TAG){
this._stateInSelfClosingTag(c);
}
/*
* attributes
*/
else if(this._state === BEFORE_ATTRIBUTE_NAME){
this._stateBeforeAttributeName(c);
} else if(this._state === IN_ATTRIBUTE_NAME){
this._stateInAttributeName(c);
} else if(this._state === AFTER_ATTRIBUTE_NAME){
this._stateAfterAttributeName(c);
} else if(this._state === BEFORE_ATTRIBUTE_VALUE){
this._stateBeforeAttributeValue(c);
} else if(this._state === IN_ATTRIBUTE_VALUE_DQ){
this._stateInAttributeValueDoubleQuotes(c);
} else if(this._state === IN_ATTRIBUTE_VALUE_SQ){
this._stateInAttributeValueSingleQuotes(c);
} else if(this._state === IN_ATTRIBUTE_VALUE_NQ){
this._stateInAttributeValueNoQuotes(c);
}
/*
* declarations
*/
else if(this._state === BEFORE_DECLARATION){
this._stateBeforeDeclaration(c);
} else if(this._state === IN_DECLARATION){
this._stateInDeclaration(c);
}
/*
* processing instructions
*/
else if(this._state === IN_PROCESSING_INSTRUCTION){
this._stateInProcessingInstruction(c);
}
/*
* comments
*/
else if(this._state === BEFORE_COMMENT){
this._stateBeforeComment(c);
} else if(this._state === IN_COMMENT){
this._stateInComment(c);
} else if(this._state === AFTER_COMMENT_1){
this._stateAfterComment1(c);
} else if(this._state === AFTER_COMMENT_2){
this._stateAfterComment2(c);
}
/*
* cdata
*/
else if(this._state === BEFORE_CDATA_1){
this._stateBeforeCdata1(c);
} else if(this._state === BEFORE_CDATA_2){
this._stateBeforeCdata2(c);
} else if(this._state === BEFORE_CDATA_3){
this._stateBeforeCdata3(c);
} else if(this._state === BEFORE_CDATA_4){
this._stateBeforeCdata4(c);
} else if(this._state === BEFORE_CDATA_5){
this._stateBeforeCdata5(c);
} else if(this._state === BEFORE_CDATA_6){
this._stateBeforeCdata6(c);
} else if(this._state === IN_CDATA){
this._stateInCdata(c);
} else if(this._state === AFTER_CDATA_1){
this._stateAfterCdata1(c);
} else if(this._state === AFTER_CDATA_2){
this._stateAfterCdata2(c);
}
/*
* special tags
*/
else if(this._state === BEFORE_SPECIAL){
this._stateBeforeSpecial(c);
} else if(this._state === BEFORE_SPECIAL_END){
this._stateBeforeSpecialEnd(c);
}
/*
* script
*/
else if(this._state === BEFORE_SCRIPT_1){
this._stateBeforeScript1(c);
} else if(this._state === BEFORE_SCRIPT_2){
this._stateBeforeScript2(c);
} else if(this._state === BEFORE_SCRIPT_3){
this._stateBeforeScript3(c);
} else if(this._state === BEFORE_SCRIPT_4){
this._stateBeforeScript4(c);
} else if(this._state === BEFORE_SCRIPT_5){
this._stateBeforeScript5(c);
}
else if(this._state === AFTER_SCRIPT_1){
this._stateAfterScript1(c);
} else if(this._state === AFTER_SCRIPT_2){
this._stateAfterScript2(c);
} else if(this._state === AFTER_SCRIPT_3){
this._stateAfterScript3(c);
} else if(this._state === AFTER_SCRIPT_4){
this._stateAfterScript4(c);
} else if(this._state === AFTER_SCRIPT_5){
this._stateAfterScript5(c);
}
/*
* style
*/
else if(this._state === BEFORE_STYLE_1){
this._stateBeforeStyle1(c);
} else if(this._state === BEFORE_STYLE_2){
this._stateBeforeStyle2(c);
} else if(this._state === BEFORE_STYLE_3){
this._stateBeforeStyle3(c);
} else if(this._state === BEFORE_STYLE_4){
this._stateBeforeStyle4(c);
}
else if(this._state === AFTER_STYLE_1){
this._stateAfterStyle1(c);
} else if(this._state === AFTER_STYLE_2){
this._stateAfterStyle2(c);
} else if(this._state === AFTER_STYLE_3){
this._stateAfterStyle3(c);
} else if(this._state === AFTER_STYLE_4){
this._stateAfterStyle4(c);
}
/*
* entities
*/
else if(this._state === BEFORE_ENTITY){
this._stateBeforeEntity(c);
} else if(this._state === BEFORE_NUMERIC_ENTITY){
this._stateBeforeNumericEntity(c);
} else if(this._state === IN_NAMED_ENTITY){
this._stateInNamedEntity(c);
} else if(this._state === IN_NUMERIC_ENTITY){
this._stateInNumericEntity(c);
} else if(this._state === IN_HEX_ENTITY){
this._stateInHexEntity(c);
}
else {
this._cbs.onerror(Error("unknown _state"), this._state);
}
this._index++;
}
this._cleanup();
};
const fs = require('fs');
const path = require('path');
const { markdownFileExts } = require('./constants');
module.exports = {
getCurrentDirectoryBase() {
return path.basename(process.cwd());
},
directoryExists(filePath) {
try {
return fs.statSync(filePath).isDirectory();
} catch (err) {
return false;
}
},
ensurePosix: (filePath) => {
if (path.sep !== '/') {
return filePath.replace(/\\/g, '/');
}
return filePath;
},
fileExists(filePath) {
try {
return fs.statSync(filePath).isFile();
} catch (err) {
return false;
}
},
getExt(file) {
const ext = file.split('.').pop();
if (!ext || ext === file) {
return '';
}
return ext;
},
wrapContent(content, front, tail) {
if (tail === undefined) {
return front + content + front;
}
return front + content + tail;
},
setExtension(filename, ext) {
return path.join(
path.dirname(filename),
path.basename(filename, path.extname(filename)) + ext,
);
},
isMarkdownFileExt(ext) {
return markdownFileExts.includes(ext);
},
isUrl(filePath) {
const r = new RegExp('^(?:[a-z]+:)?//', 'i');
return r.test(filePath);
},
isAbsolutePath(filePath) {
return path.isAbsolute(filePath)
|| filePath.includes('{{baseUrl}}')
|| filePath.includes('{{hostBaseUrl}}');
},
createErrorElement(error) {
return `<div style="color: red">${error.message}</div>`;
},
};
+20
-17
{
"name": "markbind",
"version": "1.7.0",
"version": "2.9.0",
"description": "MarkBind core module.",
"main": "lib/parser.js",
"main": "src/parser.js",
"keywords": [

@@ -16,25 +16,28 @@ "mark",

"dependencies": {
"bluebird": "^3.4.7",
"@sindresorhus/slugify": "^0.9.1",
"bluebird": "^3.7.2",
"cheerio": "^0.22.0",
"fastmatter": "^2.0.1",
"highlight.js": "^9.10.0",
"htmlparser2": "MarkBind/htmlparser2#v3.10.0-markbind.1",
"lodash": "^4.17.5",
"markdown-it": "^8.3.0",
"markdown-it-anchor": "^4.0.0",
"markdown-it-emoji": "^1.3.0",
"fastmatter": "^2.1.1",
"highlight.js": "^9.14.2",
"htmlparser2": "^3.10.1",
"lodash": "^4.17.15",
"markdown-it": "^8.4.2",
"markdown-it-anchor": "^5.2.5",
"markdown-it-attrs": "^2.4.1",
"markdown-it-emoji": "^1.4.0",
"markdown-it-imsize": "^2.0.1",
"markdown-it-ins": "^2.0.0",
"markdown-it-mark": "^2.0.0",
"markdown-it-table-of-contents": "^0.3.2",
"markdown-it-regexp": "^0.4.0",
"markdown-it-table-of-contents": "^0.4.4",
"markdown-it-task-lists": "^1.4.1",
"markdown-it-video": "^0.4.0",
"nunjucks": "^3.0.0",
"markdown-it-video": "^0.6.3",
"nunjucks": "^3.2.0",
"path-is-inside": "^1.0.2"
},
"devDependencies": {
"eslint": "^4.16.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-lodash": "^2.6.1"
"eslint": "^6.7.2",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-lodash": "^6.0.0"
},

@@ -41,0 +44,0 @@ "repository": {

const hljs = require('highlight.js');
const markdownIt = require('markdown-it')({
html: true,
linkify: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre><code class="hljs ' + lang + '">' +
hljs.highlight(lang, str, true).value +
'</code></pre>';
} catch (__) {
}
}
return '<pre class="hljs"><code>' + markdownIt.utils.escapeHtml(str) + '</code></pre>';
}
});
// markdown-it plugins
markdownIt.use(require('markdown-it-mark'))
.use(require('markdown-it-ins'))
.use(require('markdown-it-anchor'))
.use(require('markdown-it-imsize'), {autofill: false})
.use(require('markdown-it-table-of-contents'))
.use(require('markdown-it-task-lists'), {
enabled: true
})
.use(require('./markdown-it-dimmed'))
.use(require('./markdown-it-radio-button'))
.use(require('./markdown-it-block-embed'));
// fix link
markdownIt.normalizeLink = require('./normalizeLink');
// fix table style
markdownIt.renderer.rules.table_open = (tokens, idx) => {
return '<table class="markbind-table table table-bordered table-striped">';
};
// fix emoji numbers
const emojiData = require('markdown-it-emoji/lib/data/full.json');
// Extend emoji here
emojiData['zero'] = emojiData['0'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0030-20e3.png">';
emojiData['one'] = emojiData['1'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0031-20e3.png">';
emojiData['two'] = emojiData['2'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0032-20e3.png">';
emojiData['three'] = emojiData['3'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0033-20e3.png">';
emojiData['four'] = emojiData['4'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0034-20e3.png">';
emojiData['five'] = emojiData['5'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0035-20e3.png">';
emojiData['six'] = emojiData['6'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0036-20e3.png">';
emojiData['seven'] = emojiData['7'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0037-20e3.png">';
emojiData['eight'] = emojiData['8'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0038-20e3.png">';
emojiData['nine'] = emojiData['9'] = '<img style="height: 1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;" src="https://assets-cdn.github.com/images/icons/emoji/unicode/0039-20e3.png">';
markdownIt.use(require('markdown-it-emoji'), {
defs: emojiData
});
module.exports = markdownIt;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
const PluginEnvironment = require("./PluginEnvironment");
const renderer = require("./renderer");
const tokenizer = require("./tokenizer");
function setup(md, options) {
let env = new PluginEnvironment(md, options);
md.block.ruler.before("fence", "video", tokenizer.bind(env), {
alt: [ "paragraph", "reference", "blockquote", "list" ]
});
md.renderer.rules["video"] = renderer.bind(env);
}
module.exports = setup;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
'use strict';
const YouTubeService = require('./services/YouTubeService');
const VimeoService = require('./services/VimeoService');
const VineService = require('./services/VineService');
const PreziService = require('./services/PreziService');
const SlideShareService = require('./services/SlideShareService');
const PowerPointOnlineService = require('./services/PowerPointOnlineService');
class PluginEnvironment {
constructor(md, options) {
this.md = md;
this.options = Object.assign(this.getDefaultOptions(), options);
this._initServices();
}
_initServices() {
let defaultServiceBindings = {
'youtube': YouTubeService,
'vimeo': VimeoService,
'vine': VineService,
'prezi': PreziService,
'slideshare': SlideShareService,
'powerpoint': PowerPointOnlineService,
};
let serviceBindings = Object.assign({}, defaultServiceBindings, this.options.services);
let services = {};
for (let serviceName of Object.keys(serviceBindings)) {
let _serviceClass = serviceBindings[serviceName];
services[serviceName] = new _serviceClass(serviceName, this.options[serviceName], this);
}
this.services = services;
}
getDefaultOptions() {
return {
containerClassName: 'block-embed',
serviceClassPrefix: 'block-embed-service-',
outputPlayerSize: true,
allowFullScreen: true,
filterUrl: null
};
}
}
module.exports = PluginEnvironment;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
function renderer(tokens, idx, options, _env) {
let videoToken = tokens[idx];
let service = videoToken.info.service;
let videoID = videoToken.info.videoID;
return service.getEmbedCode(videoID);
}
module.exports = renderer;
"use strict";
const VideoServiceBase = require("./VideoServiceBase");
class PowerPointOnlineService extends VideoServiceBase {
getDefaultOptions() {
return {width: 610, height: 481};
}
extractVideoID(reference) {
return reference;
}
getVideoUrl(serviceUrl) {
return `${serviceUrl}&action=embedview&wdAr=1.3333333333333333`;
}
}
module.exports = PowerPointOnlineService;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
const VideoServiceBase = require("./VideoServiceBase");
class PreziService extends VideoServiceBase {
getDefaultOptions() {
return { width: 550, height: 400 };
}
extractVideoID(reference) {
let match = reference.match(/^https:\/\/prezi.com\/(.[^/]+)/);
return match ? match[1] : reference;
}
getVideoUrl(videoID) {
let escapedVideoID = this.env.md.utils.escapeHtml(videoID);
return "https://prezi.com/embed/" + escapedVideoID
+ "/?bgcolor=ffffff&amp;lock_to_path=0&amp;autoplay=0&amp;autohide_ctrls=0&amp;"
+ "landing_data=bHVZZmNaNDBIWnNjdEVENDRhZDFNZGNIUE43MHdLNWpsdFJLb2ZHanI5N1lQVHkxSHFxazZ0UUNCRHloSXZROHh3PT0&amp;"
+ "landing_sign=1kD6c0N6aYpMUS0wxnQjxzSqZlEB8qNFdxtdjYhwSuI";
}
}
module.exports = PreziService;
"use strict";
const VideoServiceBase = require("./VideoServiceBase");
class SlideShareService extends VideoServiceBase {
getDefaultOptions() {
return {width: 599, height: 487};
}
extractVideoID(reference) {
return reference;
}
getVideoUrl(videoID) {
let escapedVideoID = this.env.md.utils.escapeHtml(videoID);
return `//www.slideshare.net/slideshow/embed_code/key/${escapedVideoID}`;
}
}
module.exports = SlideShareService;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
function defaultUrlFilter(url, _videoID, _serviceName, _options) {
return url;
}
class VideoServiceBase {
constructor(name, options, env) {
this.name = name;
this.options = Object.assign(this.getDefaultOptions(), options);
this.env = env;
}
getDefaultOptions() {
return {};
}
extractVideoID(reference) {
return reference;
}
getVideoUrl(_videoID) {
throw new Error("not implemented");
}
getFilteredVideoUrl(videoID) {
let filterUrlDelegate = typeof this.env.options.filterUrl === "function"
? this.env.options.filterUrl
: defaultUrlFilter;
let videoUrl = this.getVideoUrl(videoID);
return filterUrlDelegate(videoUrl, this.name, videoID, this.env.options);
}
getEmbedCode(videoID) {
let containerClassNames = [];
if (this.env.options.containerClassName) {
containerClassNames.push(this.env.options.containerClassName);
}
let escapedServiceName = this.env.md.utils.escapeHtml(this.name);
containerClassNames.push(this.env.options.serviceClassPrefix + escapedServiceName);
let iframeAttributeList = [];
iframeAttributeList.push([ "type", "text/html" ]);
iframeAttributeList.push([ "src", this.getFilteredVideoUrl(videoID) ]);
iframeAttributeList.push([ "frameborder", 0 ]);
if (this.env.options.outputPlayerSize === true) {
if (this.options.width !== undefined && this.options.width !== null) {
iframeAttributeList.push([ "width", this.options.width ]);
}
if (this.options.height !== undefined && this.options.height !== null) {
iframeAttributeList.push([ "height", this.options.height ]);
}
}
if (this.env.options.allowFullScreen === true) {
iframeAttributeList.push([ "webkitallowfullscreen" ]);
iframeAttributeList.push([ "mozallowfullscreen" ]);
iframeAttributeList.push([ "allowfullscreen" ]);
}
let iframeAttributes = iframeAttributeList
.map(pair =>
pair[1] !== undefined
? `${pair[0]}="${pair[1]}"`
: pair[0]
)
.join(" ");
return `<div class="${containerClassNames.join(" ")}">`
+ `<iframe ${iframeAttributes}></iframe>`
+ `</div>\n`;
}
}
module.exports = VideoServiceBase;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
const VideoServiceBase = require("./VideoServiceBase");
class VimeoService extends VideoServiceBase {
getDefaultOptions() {
return { width: 500, height: 281 };
}
extractVideoID(reference) {
let match = reference.match(/https?:\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|)(\d+)(?:$|\/|\?)/);
return match && typeof match[3] === "string" ? match[3] : reference;
}
getVideoUrl(videoID) {
let escapedVideoID = this.env.md.utils.escapeHtml(videoID);
return `//player.vimeo.com/video/${escapedVideoID}`;
}
}
module.exports = VimeoService;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
const VideoServiceBase = require("./VideoServiceBase");
class VineService extends VideoServiceBase {
getDefaultOptions() {
return { width: 600, height: 600, embed: "simple" };
}
extractVideoID(reference) {
let match = reference.match(/^http(?:s?):\/\/(?:www\.)?vine\.co\/v\/([a-zA-Z0-9]{1,13}).*/);
return match && match[1].length === 11 ? match[1] : reference;
}
getVideoUrl(videoID) {
let escapedVideoID = this.env.md.utils.escapeHtml(videoID);
let escapedEmbed = this.env.md.utils.escapeHtml(this.options.embed);
return `//vine.co/v/${escapedVideoID}/embed/${escapedEmbed}`;
}
}
module.exports = VineService;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
const VideoServiceBase = require("./VideoServiceBase");
class YouTubeService extends VideoServiceBase {
getDefaultOptions() {
return { width: 640, height: 390 };
}
extractVideoID(reference) {
let match = reference.match(/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&\?]*).*/);
return match && match[7].length === 11 ? match[7] : reference;
}
getVideoUrl(videoID) {
let escapedVideoID = this.env.md.utils.escapeHtml(videoID);
return `//www.youtube.com/embed/${escapedVideoID}`;
}
}
module.exports = YouTubeService;
// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
// Licensed under the MIT license. See LICENSE file in the project root.
"use strict";
const SYNTAX_CHARS = "@[]()".split("");
const SYNTAX_CODES = SYNTAX_CHARS.map(char => char.charCodeAt(0));
function advanceToSymbol(state, endLine, symbol, pointer) {
let maxPos = null;
let symbolLine = pointer.line;
let symbolIndex = state.src.indexOf(symbol, pointer.pos);
if (symbolIndex === -1) return false;
maxPos = state.eMarks[pointer.line];
while (symbolIndex >= maxPos) {
++symbolLine;
maxPos = state.eMarks[symbolLine];
if (symbolLine >= endLine) return false;
}
pointer.prevPos = pointer.pos;
pointer.pos = symbolIndex;
pointer.line = symbolLine;
return true;
}
function tokenizer(state, startLine, endLine, silent) {
let startPos = state.bMarks[startLine] + state.tShift[startLine];
let maxPos = state.eMarks[startLine];
let pointer = { line: startLine, pos: startPos };
// Block embed must be at start of input or the previous line must be blank.
if (startLine !== 0) {
let prevLineStartPos = state.bMarks[startLine - 1] + state.tShift[startLine - 1];
let prevLineMaxPos = state.eMarks[startLine - 1];
if (prevLineMaxPos > prevLineStartPos) return false;
}
// Identify as being a potential block embed.
if (maxPos - startPos < 2) return false;
if (SYNTAX_CODES[0] !== state.src.charCodeAt(pointer.pos++)) return false;
// Read service name from within square brackets.
if (SYNTAX_CODES[1] !== state.src.charCodeAt(pointer.pos++)) return false;
if (!advanceToSymbol(state, endLine, "]", pointer)) return false;
let serviceName = state.src
.substr(pointer.prevPos, pointer.pos - pointer.prevPos)
.trim()
.toLowerCase();
++pointer.pos;
// Lookup service; if unknown, then this is not a known embed!
let service = this.services[serviceName];
if (!service) return false;
// Read embed reference from within parenthesis.
if (SYNTAX_CODES[3] !== state.src.charCodeAt(pointer.pos++)) return false;
if (!advanceToSymbol(state, endLine, ")", pointer)) return false;
let videoReference = state.src
.substr(pointer.prevPos, pointer.pos - pointer.prevPos)
.trim();
++pointer.pos;
// Do not recognize as block element when there is trailing text.
maxPos = state.eMarks[pointer.line];
let trailingText = state.src
.substr(pointer.pos, maxPos - pointer.pos)
.trim();
if (trailingText !== "") return false;
// Block embed must be at end of input or the next line must be blank.
if (endLine !== pointer.line + 1) {
let nextLineStartPos = state.bMarks[pointer.line + 1] + state.tShift[pointer.line + 1];
let nextLineMaxPos = state.eMarks[pointer.line + 1];
if (nextLineMaxPos > nextLineStartPos) return false;
}
if (pointer.line >= endLine) return false;
if (!silent) {
let token = state.push("video", "div", 0);
token.markup = state.src.slice(startPos, pointer.pos);
token.block = true;
token.info = {
serviceName: serviceName,
service: service,
videoReference: videoReference,
videoID: service.extractVideoID(videoReference)
};
token.map = [ startLine, pointer.line + 1 ];
state.line = pointer.line + 1;
}
return true;
}
module.exports = tokenizer;
'use strict';
module.exports = function dimmed_plugin(md) {
// Insert each marker as a separate text token, and add it to delimiter list
function tokenize(state, silent) {
var i, scanned, token, len, ch,
start = state.pos,
marker = state.src.charCodeAt(start);
if (silent) {
return false;
}
if (marker !== 0x25/* % */) {
return false;
}
scanned = state.scanDelims(state.pos, true);
len = scanned.length;
ch = String.fromCharCode(marker);
if (len < 2) {
return false;
}
if (len % 2) {
token = state.push('text', '', 0);
token.content = ch;
len--;
}
for (i = 0; i < len; i += 2) {
token = state.push('text', '', 0);
token.content = ch + ch;
state.delimiters.push({
marker: marker,
jump: i,
token: state.tokens.length - 1,
level: state.level,
end: -1,
open: scanned.can_open,
close: scanned.can_close
});
}
state.pos += scanned.length;
return true;
}
// Walk through delimiter list and replace text tokens with tags
//
function postProcess(state) {
var i, j,
startDelim,
endDelim,
token,
loneMarkers = [],
delimiters = state.delimiters,
max = state.delimiters.length;
for (i = 0; i < max; i++) {
startDelim = delimiters[i];
if (startDelim.marker !== 0x25/* % */) {
continue;
}
if (startDelim.end === -1) {
continue;
}
endDelim = delimiters[startDelim.end];
token = state.tokens[startDelim.token];
token.type = 'dimmed_open';
token.tag = 'span';
token.attrs = [['class', 'dimmed']];
token.nesting = 1;
token.markup = '%%';
token.content = '';
token = state.tokens[endDelim.token];
token.type = 'dimmed_close';
token.tag = 'span';
token.nesting = -1;
token.markup = '%%';
token.content = '';
if (state.tokens[endDelim.token - 1].type === 'text' &&
state.tokens[endDelim.token - 1].content === '%') {
loneMarkers.push(endDelim.token - 1);
}
}
// If a marker sequence has an odd number of characters, it's splitted
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
// start of the sequence.
//
// So, we have to move all those markers after subsequent s_close tags.
//
while (loneMarkers.length) {
i = loneMarkers.pop();
j = i + 1;
while (j < state.tokens.length && state.tokens[j].type === 'dimmed_close') {
j++;
}
j--;
if (i !== j) {
token = state.tokens[j];
state.tokens[j] = state.tokens[i];
state.tokens[i] = token;
}
}
}
md.inline.ruler.before('emphasis', 'dimmed', tokenize);
md.inline.ruler2.before('emphasis', 'dimmed', postProcess);
};
const crypto = require('crypto');
var disableRadio = false;
var useLabelWrapper = true;
/**
* Modified from https://github.com/revin/markdown-it-task-lists/blob/master/index.js
*/
module.exports = function(md, options) {
if (options) {
disableRadio = !options.enabled;
useLabelWrapper = !!options.label;
}
md.core.ruler.after('inline', 'radio-lists', function(state) {
var tokens = state.tokens;
for (var i = 2; i < tokens.length; i++) {
if (isTodoItem(tokens, i)) {
var group = attrGet(tokens[parentToken(tokens, i-2)], 'radio-group'); // try retrieve the group id
if (group) {
group = group[1];
} else {
group = crypto.createHash('md5')
.update(tokens[i-5].content)
.update(tokens[i-4].content)
.update(tokens[i].content).digest('hex').substr(2, 5); // generate a deterministic group id
}
radioify(tokens[i], state.Token, group);
attrSet(tokens[i-2], 'class', 'radio-list-item');
attrSet(tokens[parentToken(tokens, i-2)], 'radio-group', group); // save the group id to the top-level list
attrSet(tokens[parentToken(tokens, i-2)], 'class', 'radio-list');
}
}
});
};
function attrSet(token, name, value) {
var index = token.attrIndex(name);
var attr = [name, value];
if (index < 0) {
token.attrPush(attr);
} else {
token.attrs[index] = attr;
}
}
function attrGet(token, name) {
var index = token.attrIndex(name);
if (index < 0) {
return void(0);
} else {
return token.attrs[index];
}
}
function parentToken(tokens, index) {
var targetLevel = tokens[index].level - 1;
for (var i = index - 1; i >= 0; i--) {
if (tokens[i].level === targetLevel) {
return i;
}
}
return -1;
}
function isTodoItem(tokens, index) {
return isInline(tokens[index]) &&
isParagraph(tokens[index - 1]) &&
isListItem(tokens[index - 2]) &&
startsWithTodoMarkdown(tokens[index]);
}
function radioify(token, TokenConstructor, radioId) {
token.children.unshift(makeRadioButton(token, TokenConstructor, radioId));
token.children[1].content = token.children[1].content.slice(3);
token.content = token.content.slice(3);
if (useLabelWrapper) {
token.children.unshift(beginLabel(TokenConstructor));
token.children.push(endLabel(TokenConstructor));
}
}
function makeRadioButton(token, TokenConstructor, radioId) {
var radio = new TokenConstructor('html_inline', '', 0);
var disabledAttr = disableRadio ? ' disabled="" ' : '';
if (token.content.indexOf('( ) ') === 0) {
radio.content = '<input class="radio-list-input" name="' + radioId + '"' + disabledAttr + 'type="radio">';
} else if (token.content.indexOf('(x) ') === 0 || token.content.indexOf('(X) ') === 0) {
radio.content = '<input class="radio-list-input" checked="" name="' + radioId + '"' + disabledAttr + 'type="radio">';
}
return radio;
}
// these next two functions are kind of hacky; probably should really be a
// true block-level token with .tag=='label'
function beginLabel(TokenConstructor) {
var token = new TokenConstructor('html_inline', '', 0);
token.content = '<label>';
return token;
}
function endLabel(TokenConstructor) {
var token = new TokenConstructor('html_inline', '', 0);
token.content = '</label>';
return token;
}
function isInline(token) { return token.type === 'inline'; }
function isParagraph(token) { return token.type === 'paragraph_open'; }
function isListItem(token) { return token.type === 'list_item_open'; }
function startsWithTodoMarkdown(token) {
// leading whitespace in a list item is already trimmed off by markdown-it
return token.content.indexOf('( ) ') === 0 || token.content.indexOf('(x) ') === 0 || token.content.indexOf('(X) ') === 0;
}
let mdurl = require('mdurl');
let punycode = require('punycode');
let RECODE_HOSTNAME_FOR = [ 'http:', 'https:', 'mailto:' ];
mdurl.encode.defaultChars = mdurl.encode.defaultChars += '{}';
function normalizeLink(url) {
var parsed = mdurl.parse(url, true);
if (parsed.hostname) {
// Encode hostnames in urls like:
// `http://host/`, `https://host/`, `mailto:user@host`, `//host/`
//
// We don't encode unknown schemas, because it's likely that we encode
// something we shouldn't (e.g. `skype:name` treated as `skype:host`)
//
if (!parsed.protocol || RECODE_HOSTNAME_FOR.indexOf(parsed.protocol) >= 0) {
try {
parsed.hostname = punycode.toASCII(parsed.hostname);
} catch (er) { /**/ }
}
}
return mdurl.encode(mdurl.format(parsed));
}
module.exports = normalizeLink;
/* eslint-disable no-underscore-dangle */
const htmlparser = require('htmlparser2');
const md = require('./markdown-it');
const _ = {};
_.clone = require('lodash/clone');
_.cloneDeep = require('lodash/cloneDeep');
_.hasIn = require('lodash/hasIn');
_.isArray = require('lodash/isArray');
_.isEmpty = require('lodash/isEmpty');
_.pick = require('lodash/pick');
const Promise = require('bluebird');
const cheerio = require('cheerio');
cheerio.prototype.options.xmlMode = true; // Enable xml mode for self-closing tag
cheerio.prototype.options.decodeEntities = false; // Don't escape HTML entities
const utils = require('./utils');
const fs = require('fs');
const path = require('path');
const pathIsInside = require('path-is-inside');
const url = require('url');
const nunjucks = require('nunjucks');
const ATTRIB_INCLUDE_PATH = 'include-path';
const ATTRIB_CWF = 'cwf';
const BOILERPLATE_FOLDER_NAME = '_markbind/boilerplates';
/*
* Utils
*/
/**
* @throws Will throw an error if a non-absolute path or path outside the root is given
*/
function calculateNewBaseUrl(filePath, root, lookUp) {
if (!path.isAbsolute(filePath)) {
throw new Error(`Non-absolute path given to calculateNewBaseUrl: '${filePath}'`);
}
if (!pathIsInside(filePath, root)) {
throw new Error(`Path given '${filePath}' is not in root '${root}'`);
}
function calculate(file, result) {
if (file === root) {
return { relative: path.relative(root, root), parent: root };
}
const parent = path.dirname(file);
if (lookUp[parent] && result.length === 1) {
return { relative: path.relative(parent, result[0]), parent };
} else if (lookUp[parent]) {
return calculate(parent, [parent]);
}
return calculate(parent, result);
}
return calculate(filePath, []);
}
function calculateBoilerplateFilePath(pathInBoilerplates, asIfAt, config) {
const fileBase = calculateNewBaseUrl(asIfAt, config.rootPath, config.baseUrlMap).relative;
return path.resolve(fileBase, BOILERPLATE_FOLDER_NAME, pathInBoilerplates);
}
function createErrorNode(element, error) {
const errorElement = cheerio.parseHTML(utils.createErrorElement(error), true)[0];
return Object.assign(element, _.pick(errorElement, ['name', 'attribs', 'children']));
}
function isText(element) {
return element.type === 'text' || element.type === 'comment';
}
function Parser(options) {
this._options = options || {};
// eslint-disable-next-line no-console
this._onError = this._options.errorHandler || console.error;
this._fileCache = {};
this.dynamicIncludeSrc = [];
this.staticIncludeSrc = [];
this.boilerplateIncludeSrc = [];
this.missingIncludeSrc = [];
}
Parser.prototype.getDynamicIncludeSrc = function () {
return _.clone(this.dynamicIncludeSrc);
};
Parser.prototype.getStaticIncludeSrc = function () {
return _.clone(this.staticIncludeSrc);
};
Parser.prototype.getBoilerplateIncludeSrc = function () {
return _.clone(this.boilerplateIncludeSrc);
};
Parser.prototype.getMissingIncludeSrc = function () {
return _.clone(this.missingIncludeSrc);
};
Parser.prototype._preprocess = function (node, context, config) {
const element = node;
const self = this;
element.attribs = element.attribs || {};
element.attribs[ATTRIB_CWF] = path.resolve(context.cwf);
const requiresSrc = ['include', 'dynamic-panel'].includes(element.name);
if (requiresSrc && _.isEmpty(element.attribs.src)) {
const error = new Error(`Empty src attribute in ${element.name} in: ${element.attribs[ATTRIB_CWF]}`);
this._onError(error);
return createErrorNode(element, error);
}
const shouldProcessSrc = ['include', 'dynamic-panel', 'panel', 'morph'].includes(element.name);
const hasSrc = _.hasIn(element.attribs, 'src');
let isUrl;
let includeSrc;
let filePath;
let actualFilePath;
if (hasSrc && shouldProcessSrc) {
isUrl = utils.isUrl(element.attribs.src);
includeSrc = url.parse(element.attribs.src);
filePath = isUrl ? element.attribs.src : path.resolve(path.dirname(context.cwf), includeSrc.path);
actualFilePath = filePath;
const isBoilerplate = _.hasIn(element.attribs, 'boilerplate');
if (isBoilerplate) {
element.attribs.boilerplate = element.attribs.boilerplate || path.basename(filePath);
actualFilePath = calculateBoilerplateFilePath(element.attribs.boilerplate, filePath, config);
this.boilerplateIncludeSrc.push({ from: context.cwf, to: actualFilePath });
}
if (!utils.fileExists(actualFilePath)) {
this.missingIncludeSrc.push({ from: context.cwf, to: actualFilePath });
const error = new Error(
`No such file: ${actualFilePath}\nMissing reference in ${element.attribs[ATTRIB_CWF]}`,
);
this._onError(error);
return createErrorNode(element, error);
}
}
if (element.name === 'include') {
const isInline = _.hasIn(element.attribs, 'inline');
const isDynamic = _.hasIn(element.attribs, 'dynamic');
element.name = isInline ? 'span' : 'div';
element.attribs[ATTRIB_INCLUDE_PATH] = filePath;
if (isDynamic) {
element.name = 'dynamic-panel';
element.attribs.src = filePath;
if (includeSrc.hash) {
element.attribs.fragment = includeSrc.hash.substring(1);
}
element.attribs.header = element.attribs.name || '';
delete element.attribs.dynamic;
this.dynamicIncludeSrc.push({ from: context.cwf, to: actualFilePath, asIfTo: element.attribs.src });
return element;
}
if (isUrl) {
return element; // only keep url path for dynamic
}
this.staticIncludeSrc.push({ from: context.cwf, to: actualFilePath });
try {
self._fileCache[actualFilePath] = self._fileCache[actualFilePath]
? self._fileCache[actualFilePath] : fs.readFileSync(actualFilePath, 'utf8');
} catch (e) {
// Read file fail
const missingReferenceErrorMessage = `Missing reference in: ${element.attribs[ATTRIB_CWF]}`;
e.message += `\n${missingReferenceErrorMessage}`;
this._onError(e);
return createErrorNode(element, e);
}
const isIncludeSrcMd = utils.getExtName(filePath) === 'md';
if (isIncludeSrcMd && context.source === 'html') {
// HTML include markdown, use special tag to indicate markdown code.
element.name = 'markdown';
}
let fileContent = self._fileCache[actualFilePath]; // cache the file contents to save some I/O
const fileBase = path.resolve(calculateNewBaseUrl(filePath, config.rootPath, config.baseUrlMap).relative);
const userDefinedVariables = config.userDefinedVariablesMap[fileBase];
fileContent = nunjucks.renderString(fileContent, userDefinedVariables);
delete element.attribs.boilerplate;
delete element.attribs.src;
delete element.attribs.inline;
if (includeSrc.hash) {
// directly get segment from the src
const segmentSrc = cheerio.parseHTML(fileContent, true);
const $ = cheerio.load(segmentSrc);
const htmlContent = $(includeSrc.hash).html();
let actualContent = htmlContent;
if (isIncludeSrcMd) {
if (context.mode === 'include') {
actualContent = isInline ? actualContent : utils.wrapContent(actualContent, '\n\n', '\n');
} else {
actualContent = md.render(actualContent);
}
actualContent = self._rebaseReferenceForStaticIncludes(actualContent, element, config);
}
element.children = cheerio.parseHTML(actualContent, true); // the needed content;
} else {
let actualContent = fileContent;
if (isIncludeSrcMd) {
if (context.mode === 'include') {
actualContent = isInline ? actualContent : utils.wrapContent(actualContent, '\n\n', '\n');
} else {
actualContent = md.render(actualContent);
}
}
element.children = cheerio.parseHTML(actualContent, true);
}
// The element's children are in the new context
// Process with new context
const childContext = _.cloneDeep(context);
childContext.cwf = filePath;
childContext.source = isIncludeSrcMd ? 'md' : 'html';
if (element.children && element.children.length > 0) {
element.children = element.children.map(e => self._preprocess(e, childContext, config));
}
} else if (element.name === 'dynamic-panel') {
if (!isUrl && includeSrc.hash) {
element.attribs.fragment = includeSrc.hash.substring(1); // save hash to fragment attribute
}
element.attribs.src = filePath;
this.dynamicIncludeSrc.push({ from: context.cwf, to: actualFilePath, asIfTo: filePath });
return element;
} else if ((element.name === 'morph' || element.name === 'panel') && hasSrc) {
if (!isUrl && includeSrc.hash) {
element.attribs.fragment = includeSrc.hash.substring(1); // save hash to fragment attribute
}
element.attribs.src = filePath;
this.dynamicIncludeSrc.push({ from: context.cwf, to: actualFilePath, asIfTo: filePath });
return element;
} else if (element.children && element.children.length > 0) {
element.children = element.children.map(e => self._preprocess(e, context, config));
}
return element;
};
Parser.prototype._parse = function (node, context, config) {
const element = node;
const self = this;
if (_.isArray(element)) {
return element.map(el => self._parse(el, context, config));
}
if (isText(element)) {
return element;
}
if (element.name) {
element.name = element.name.toLowerCase();
}
switch (element.name) {
case 'md':
element.name = 'span';
cheerio.prototype.options.xmlMode = false;
element.children = cheerio.parseHTML(md.renderInline(cheerio.html(element.children)), true);
cheerio.prototype.options.xmlMode = true;
break;
case 'markdown':
element.name = 'div';
cheerio.prototype.options.xmlMode = false;
element.children = cheerio.parseHTML(md.render(cheerio.html(element.children)), true);
cheerio.prototype.options.xmlMode = true;
break;
case 'dynamic-panel': {
const hasIsOpen = _.hasIn(element.attribs, 'isOpen');
element.attribs.isOpen = hasIsOpen || false;
const fileExists = utils.fileExists(element.attribs.src)
|| utils.fileExists(calculateBoilerplateFilePath(element.attribs.boilerplate,
element.attribs.src, config));
if (fileExists) {
const { src, fragment } = element.attribs;
const resultDir = path.dirname(path.join('{{hostBaseUrl}}', path.relative(process.cwd(), src)));
const resultPath = path.join(resultDir, utils.setExtension(path.basename(src), '._include_.html'));
element.attribs.src = fragment ? `${resultPath}#${fragment}` : resultPath;
}
element.attribs['no-close'] = 'true';
element.attribs['no-switch'] = 'true';
element.name = 'panel';
delete element.attribs.boilerplate;
break;
}
case 'morph': {
// eslint-disable-next-line no-console
console.warn('DeprecationWarning: morph is deprecated. Consider using panel instead.');
if (!_.hasIn(element.attribs, 'src')) {
break;
}
const fileExists = utils.fileExists(element.attribs.src)
|| utils.fileExists(calculateBoilerplateFilePath(element.attribs.boilerplate,
element.attribs.src, config));
if (fileExists) {
const { src, fragment } = element.attribs;
const resultDir = path.dirname(path.join('{{hostBaseUrl}}', path.relative(process.cwd(), src)));
const resultPath = path.join(resultDir, utils.setExtension(path.basename(src), '._include_.html'));
element.attribs.src = fragment ? `${resultPath}#${fragment}` : resultPath;
}
delete element.attribs.boilerplate;
break;
}
case 'panel': {
if (!_.hasIn(element.attribs, 'src')) { // dynamic panel
break;
}
const fileExists = utils.fileExists(element.attribs.src)
|| utils.fileExists(calculateBoilerplateFilePath(element.attribs.boilerplate,
element.attribs.src, config));
if (fileExists) {
const { src, fragment } = element.attribs;
const resultDir = path.dirname(path.join('{{hostBaseUrl}}', path.relative(process.cwd(), src)));
const resultPath = path.join(resultDir, utils.setExtension(path.basename(src), '._include_.html'));
element.attribs.src = fragment ? `${resultPath}#${fragment}` : resultPath;
}
delete element.attribs.boilerplate;
break;
}
default:
break;
}
if (element.children) {
element.children.forEach((child) => {
self._parse(child, context, config);
});
}
return element;
};
Parser.prototype._trimNodes = function (node) {
const self = this;
if (node.name === 'pre' || node.name === 'code') {
return;
}
if (node.children) {
/* eslint-disable no-plusplus */
for (let n = 0; n < node.children.length; n++) {
const child = node.children[n];
if (
child.type === 'comment'
|| (child.type === 'text' && n === node.children.length - 1 && !/\S/.test(child.data))
) {
node.children.splice(n, 1);
n--;
} else if (child.type === 'tag') {
self._trimNodes(child);
}
}
/* eslint-enable no-plusplus */
}
};
Parser.prototype.includeFile = function (file, config) {
const context = {};
context.cwf = config.cwf || file; // current working file
context.mode = 'include';
return new Promise((resolve, reject) => {
const handler = new htmlparser.DomHandler((error, dom) => {
if (error) {
reject(error);
return;
}
const nodes = dom.map((d) => {
let processed;
try {
processed = this._preprocess(d, context, config);
} catch (err) {
err.message += `\nError while preprocessing '${file}'`;
this._onError(err);
processed = createErrorNode(d, err);
}
return processed;
});
resolve(cheerio.html(nodes));
});
const parser = new htmlparser.Parser(handler, {
xmlMode: true,
decodeEntities: true,
});
let actualFilePath = file;
if (!utils.fileExists(file)) {
const boilerplateFilePath = calculateBoilerplateFilePath(path.basename(file), file, config);
if (utils.fileExists(boilerplateFilePath)) {
actualFilePath = boilerplateFilePath;
}
}
// Read files
fs.readFile(actualFilePath, 'utf-8', (err, data) => {
if (err) {
reject(err);
return;
}
const fileBase = path.resolve(calculateNewBaseUrl(file, config.rootPath, config.baseUrlMap).relative);
const userDefinedVariables = config.userDefinedVariablesMap[fileBase];
const fileContent = nunjucks.renderString(data, userDefinedVariables);
const fileExt = utils.getExtName(file);
if (fileExt === 'md') {
context.source = 'md';
parser.parseComplete(fileContent.toString());
} else if (fileExt === 'html') {
context.source = 'html';
parser.parseComplete(fileContent);
} else {
const error = new Error(`Unsupported File Extension: '${fileExt}'`);
reject(error);
}
});
});
};
Parser.prototype.renderFile = function (file, config) {
const context = {};
context.cwf = file; // current working file
context.mode = 'render';
return new Promise((resolve, reject) => {
const handler = new htmlparser.DomHandler((error, dom) => {
if (error) {
reject(error);
return;
}
const nodes = dom.map((d) => {
let parsed;
try {
parsed = this._parse(d, context, config);
} catch (err) {
err.message += `\nError while rendering '${file}'`;
this._onError(err);
parsed = createErrorNode(d, err);
}
return parsed;
});
nodes.forEach((d) => {
this._trimNodes(d);
});
cheerio.prototype.options.xmlMode = false;
resolve(cheerio.html(nodes));
cheerio.prototype.options.xmlMode = true;
});
const parser = new htmlparser.Parser(handler, {
xmlMode: true,
decodeEntities: false,
});
// Read files
fs.readFile(file, (err, data) => {
if (err) {
reject(err);
return;
}
const fileExt = utils.getExtName(file);
if (fileExt === 'md') {
const inputData = md.render(data.toString());
context.source = 'md';
parser.parseComplete(inputData);
} else if (fileExt === 'html') {
context.source = 'html';
parser.parseComplete(data);
} else {
const error = new Error(`Unsupported File Extension: '${fileExt}'`);
reject(error);
}
});
});
};
Parser.prototype.resolveBaseUrl = function (pageData, config) {
const { baseUrlMap, rootPath, isDynamic } = config;
this.baseUrlMap = baseUrlMap;
this.rootPath = rootPath;
this.isDynamic = isDynamic || false;
if (this.isDynamic) {
this.dynamicSource = config.dynamicSource;
}
return new Promise((resolve, reject) => {
const handler = new htmlparser.DomHandler((error, dom) => {
if (error) {
reject(error);
return;
}
const nodes = dom.map((d) => {
const node = d;
const childrenBase = {};
if (this.isDynamic) {
// Change CWF for each top level element
if (node.attribs) {
node.attribs[ATTRIB_CWF] = this.dynamicSource;
}
}
return this._rebaseReference(node, childrenBase);
});
cheerio.prototype.options.xmlMode = false;
resolve(cheerio.html(nodes));
cheerio.prototype.options.xmlMode = true;
});
const parser = new htmlparser.Parser(handler, {
xmlMode: true,
decodeEntities: true,
});
parser.parseComplete(pageData);
});
};
Parser.prototype._rebaseReference = function (node, foundBase) {
const element = node;
if (_.isArray(element)) {
return element.map(el => this._rebaseReference(el, foundBase));
}
if (isText(element)) {
return element;
}
// Rebase children element
const childrenBase = {};
element.children.forEach((el) => {
this._rebaseReference(el, childrenBase);
});
// rebase current element
if (element.attribs[ATTRIB_CWF]) {
const filePath = element.attribs[ATTRIB_CWF];
let newBase = calculateNewBaseUrl(filePath, this.rootPath, this.baseUrlMap);
if (newBase) {
const { relative, parent } = newBase;
// eslint-disable-next-line no-param-reassign
foundBase[parent] = relative;
}
const bases = Object.keys(childrenBase);
if (bases.length !== 0) {
// need to rebase
newBase = childrenBase[bases[0]];
const { children } = element;
if (children) {
const currentBase = calculateNewBaseUrl(element.attribs[ATTRIB_CWF], this.rootPath, this.baseUrlMap);
if (currentBase) {
if (currentBase.relative !== newBase) {
cheerio.prototype.options.xmlMode = false;
const newBaseUrl = `{{hostBaseUrl}}/${newBase}`;
const rendered = nunjucks.renderString(cheerio.html(children), { baseUrl: newBaseUrl });
element.children = cheerio.parseHTML(rendered, true);
cheerio.prototype.options.xmlMode = true;
}
}
}
}
delete element.attribs[ATTRIB_INCLUDE_PATH];
}
delete element.attribs[ATTRIB_CWF];
return element;
};
Parser.prototype._rebaseReferenceForStaticIncludes = function (pageData, element, config) {
if (!config) {
return pageData;
}
if (!pageData.includes('{{baseUrl}}')) {
return pageData;
}
const filePath = element.attribs[ATTRIB_INCLUDE_PATH];
const fileBase = calculateNewBaseUrl(filePath, config.rootPath, config.baseUrlMap);
if (!fileBase.relative) {
return pageData;
}
const currentPath = element.attribs[ATTRIB_CWF];
const currentBase = calculateNewBaseUrl(currentPath, config.rootPath, config.baseUrlMap);
if (currentBase.relative === fileBase.relative) {
return pageData;
}
const newBase = fileBase.relative;
const newBaseUrl = `{{hostBaseUrl}}/${newBase}`;
return nunjucks.renderString(pageData, { baseUrl: newBaseUrl });
};
module.exports = Parser;
const fs = require('fs');
const path = require('path');
module.exports = {
getCurrentDirectoryBase() {
return path.basename(process.cwd());
},
directoryExists(filePath) {
try {
return fs.statSync(filePath).isDirectory();
} catch (err) {
return false;
}
},
fileExists(filePath) {
try {
return fs.statSync(filePath).isFile();
} catch (err) {
return false;
}
},
getExtName(file) {
const ext = file.split('.').pop();
if (!ext || ext === file) {
return '';
}
return ext;
},
wrapContent(content, front, tail) {
if (tail === undefined) {
return front + content + front;
}
return front + content + tail;
},
setExtension(filename, ext) {
return path.join(
path.dirname(filename),
path.basename(filename, path.extname(filename)) + ext,
);
},
isUrl(filePath) {
const r = new RegExp('^(?:[a-z]+:)?//', 'i');
return r.test(filePath);
},
createErrorElement(error) {
return `<div style="color: red">${error.message}</div>`;
},
};