| 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&lock_to_path=0&autoplay=0&autohide_ctrls=0&" | ||
| + "landing_data=bHVZZmNaNDBIWnNjdEVENDRhZDFNZGNIUE43MHdLNWpsdFJLb2ZHanI5N1lQVHkxSHFxazZ0UUNCRHloSXZROHh3PT0&" | ||
| + "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; |
+905
| 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(); | ||
| }; |
+75
| 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&lock_to_path=0&autoplay=0&autohide_ctrls=0&" | ||
| + "landing_data=bHVZZmNaNDBIWnNjdEVENDRhZDFNZGNIUE43MHdLNWpsdFJLb2ZHanI5N1lQVHkxSHFxazZ0UUNCRHloSXZROHh3PT0&" | ||
| + "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; |
-600
| /* 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; |
-55
| 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>`; | ||
| }, | ||
| }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
GitHub dependency
Supply chain riskContains a dependency which resolves to a GitHub URL. Dependencies fetched from GitHub specifiers are not immutable can be used to inject untrusted code or reduce the likelihood of a reproducible install.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
83781
81.41%23
27.78%2095
78.3%0
-100%20
17.65%12
300%2
100%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated