diff --git a/.eslintignore b/.eslintignore index 1122886a22..cc44517afa 100644 --- a/.eslintignore +++ b/.eslintignore @@ -17,6 +17,7 @@ packages/core/src/Layout/*.js packages/core/src/lib/nunjucks-extensions/*.js packages/core/src/lib/markdown-it/index.js packages/core/src/lib/markdown-it/highlight/*.js +packages/core/src/lib/markdown-it/plugins/**/*.js packages/core/src/lib/markdown-it/utils/*.js packages/core/src/Page/*.js packages/core/src/plugins/**/*.js diff --git a/.gitignore b/.gitignore index 8d1f6fa8cc..9e363394d6 100644 --- a/.gitignore +++ b/.gitignore @@ -93,8 +93,7 @@ packages/core/src/Layout/*.js packages/core/src/lib/nunjucks-extensions/*.js packages/core/src/lib/markdown-it/index.js packages/core/src/lib/markdown-it/highlight/*.js -packages/core/src/lib/markdown-it/plugins/markdown-it-icons.js -packages/core/src/lib/markdown-it/plugins/markdown-it-alt-frontmatter.js +packages/core/src/lib/markdown-it/plugins/**/*.js packages/core/src/lib/markdown-it/utils/*.js packages/core/src/Page/*.js packages/core/src/plugins/**/*.js diff --git a/packages/core/src/lib/markdown-it/index.ts b/packages/core/src/lib/markdown-it/index.ts index a974acd815..4cb38a00cc 100644 --- a/packages/core/src/lib/markdown-it/index.ts +++ b/packages/core/src/lib/markdown-it/index.ts @@ -12,8 +12,12 @@ import { Highlighter } from './highlight/Highlighter'; import { altFrontmatterPlugin } from './plugins/markdown-it-alt-frontmatter'; import { markdownItIconsPlugin } from './plugins/markdown-it-icons'; - -const createDoubleDelimiterInlineRule = require('./plugins/markdown-it-double-delimiter'); +import { centertext_plugin } from './plugins/markdown-it-center-text'; +import { colourTextPlugin } from './plugins/markdown-it-colour-text'; +import { createDoubleDelimiterInlineRule } from './plugins/markdown-it-double-delimiter'; +import { footnotePlugin } from './plugins/markdown-it-footnotes'; +import { radioButtonPlugin } from './plugins/markdown-it-radio-button'; +import blockEmbedPlugin from './plugins/markdown-it-block-embed'; const markdownIt = markdownItImport({ html: true, linkify: true }); @@ -38,12 +42,12 @@ markdownIt.use(require('markdown-it-mark')) .use(require('markdown-it-linkify-images'), { imgClass: 'img-fluid' }) .use(require('markdown-it-texmath'), { engine: katex, delimiters: ['dollars', 'brackets'] }) .use(require('markdown-it-attrs')) - .use(require('./plugins/markdown-it-radio-button')) - .use(require('./plugins/markdown-it-block-embed')) + .use(radioButtonPlugin, { enabled: true, label: true }) + .use(blockEmbedPlugin) .use(markdownItIconsPlugin) - .use(require('./plugins/markdown-it-footnotes')) - .use(require('./plugins/markdown-it-center-text')) - .use(require('./plugins/markdown-it-colour-text')) + .use(footnotePlugin) + .use(centertext_plugin) + .use(colourTextPlugin) .use(altFrontmatterPlugin); // fix table style @@ -236,4 +240,5 @@ markdownIt.use(require('markdown-it-emoji'), { defs: fixedNumberEmojiDefs, }); +(markdownIt as any).createDoubleDelimiterInlineRule = createDoubleDelimiterInlineRule; export = markdownIt; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/PluginEnvironment.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/PluginEnvironment.js deleted file mode 100644 index c3be1bdbf7..0000000000 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/PluginEnvironment.js +++ /dev/null @@ -1,56 +0,0 @@ -// 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; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/PluginEnvironment.ts b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/PluginEnvironment.ts new file mode 100644 index 0000000000..9a97be1177 --- /dev/null +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/PluginEnvironment.ts @@ -0,0 +1,63 @@ +// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors +// Licensed under the MIT license. See LICENSE file in the project root. + +import MarkdownIt from 'markdown-it'; +import { EmbedServiceMap } from './tokenizer'; +import { BlockEmbedOptions } from './index'; + +import YouTubeService from './services/YouTubeService'; +import VimeoService from './services/VimeoService'; +import VineService from './services/VineService'; +import PreziService from './services/PreziService'; +import SlideShareService from './services/SlideShareService'; +import PowerPointOnlineService from './services/PowerPointOnlineService'; + +export default class PluginEnvironment { + public md: MarkdownIt; + public options: BlockEmbedOptions; + public services: EmbedServiceMap = {}; + + constructor(md: MarkdownIt, options: BlockEmbedOptions) { + this.md = md; + this.options = Object.assign(this.getDefaultOptions(), options); + + this._initServices(); + } + + _initServices(): void { + const defaultServiceBindings: Record = { + 'youtube': YouTubeService, + 'vimeo': VimeoService, + 'vine': VineService, + 'prezi': PreziService, + 'slideshare': SlideShareService, + 'powerpoint': PowerPointOnlineService, + }; + + let serviceBindings = Object.assign({}, defaultServiceBindings, this.options.services); + let services: EmbedServiceMap = {}; + for (let serviceName of Object.keys(serviceBindings)) { + let _serviceClass = serviceBindings[serviceName]; + const ActualConstructor = _serviceClass.default || _serviceClass; + + if (typeof ActualConstructor === 'function') { + services[serviceName] = new ActualConstructor(serviceName, this.options[serviceName], this); + } else { + console.error(`BlockEmbed Error: Service "${serviceName}" is not a valid constructor.`); + } + } + + this.services = services; + } + + public getDefaultOptions(): BlockEmbedOptions { + return { + containerClassName: 'block-embed', + serviceClassPrefix: 'block-embed-service-', + outputPlayerSize: true, + allowFullScreen: true, + filterUrl: null + }; + } + +} diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/index.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/index.js deleted file mode 100644 index b0b6770268..0000000000 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/index.js +++ /dev/null @@ -1,21 +0,0 @@ -// 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; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/index.ts b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/index.ts new file mode 100644 index 0000000000..d855ce21f0 --- /dev/null +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/index.ts @@ -0,0 +1,35 @@ +// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors +// Licensed under the MIT license. See LICENSE file in the project root. + +import MarkdownIt from 'markdown-it'; + +import PluginEnvironment from './PluginEnvironment'; +import renderer from './renderer'; +import { createTokenizer } from './tokenizer'; + +export type UrlFilterDelegate = ( + url: string, + serviceName: string, + videoID: string, + options: BlockEmbedOptions +) => string; + +export interface BlockEmbedOptions { + containerClassName?: string; + serviceClassPrefix?: string; + outputPlayerSize?: boolean; + allowFullScreen?: boolean; + filterUrl?: UrlFilterDelegate | null; + services?: Record; + [serviceConfig: string]: any; +} + +export default function setup(md: MarkdownIt, options?: BlockEmbedOptions) { + const normalizedOptions: BlockEmbedOptions = options ?? {}; + let env = new PluginEnvironment(md, normalizedOptions); + + md.block.ruler.before("fence", "video", createTokenizer(env.services), { + alt: [ "paragraph", "reference", "blockquote", "list" ] + }); + md.renderer.rules["video"] = renderer.bind(env); +} diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/renderer.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/renderer.js deleted file mode 100644 index cf86fc31ef..0000000000 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/renderer.js +++ /dev/null @@ -1,17 +0,0 @@ -// 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; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/renderer.ts b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/renderer.ts new file mode 100644 index 0000000000..a27c6df3dd --- /dev/null +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/renderer.ts @@ -0,0 +1,13 @@ +// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors +// Licensed under the MIT license. See LICENSE file in the project root. + +import Token from 'markdown-it/lib/token'; + +export default function renderer(tokens: Token[], idx: number, _options: any, _env: any): string { + let videoToken = tokens[idx]; + + let service = (videoToken as any).info.service; + let videoID = (videoToken as any).info.videoID; + + return service.getEmbedCode(videoID); +} diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/PowerPointOnlineService.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/PowerPointOnlineService.js deleted file mode 100644 index 1f7280adf5..0000000000 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/PowerPointOnlineService.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; - -const VideoServiceBase = require("./VideoServiceBase"); - -class PowerPointOnlineService extends VideoServiceBase { - - getDefaultOptions() { - return { width: 610, height: 481, ignoreStyle: true }; - } - - extractVideoID(reference) { - return reference; - } - - getVideoUrl(serviceUrl) { - return `${serviceUrl}&action=embedview&wdAr=1.3333333333333333`; - } -} - -module.exports = PowerPointOnlineService; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/PowerPointOnlineService.ts b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/PowerPointOnlineService.ts new file mode 100644 index 0000000000..935aed3c08 --- /dev/null +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/PowerPointOnlineService.ts @@ -0,0 +1,21 @@ +import VideoServiceBase, { VideoServiceOptions } from './VideoServiceBase'; + +export interface PowerPointOnlineOptions extends VideoServiceOptions { + width?: number; + height?: number; +} + +export default class PowerPointOnlineService extends VideoServiceBase { + + getDefaultOptions(): PowerPointOnlineOptions { + return { width: 610, height: 481, ignoreStyle: true }; + } + + extractVideoID(reference: string): string { + return reference; + } + + getVideoUrl(serviceUrl: string): string { + return `${serviceUrl}&action=embedview&wdAr=1.3333333333333333`; + } +} diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/PreziService.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/PreziService.ts similarity index 66% rename from packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/PreziService.js rename to packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/PreziService.ts index 163f0f0228..e6be2fe148 100644 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/PreziService.js +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/PreziService.ts @@ -1,23 +1,26 @@ // 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"; +import VideoServiceBase, { VideoServiceOptions } from './VideoServiceBase'; -const VideoServiceBase = require("./VideoServiceBase"); +export interface PreziOptions extends VideoServiceOptions { + width?: number; + height?: number; +} -class PreziService extends VideoServiceBase { +export default class PreziService extends VideoServiceBase { - getDefaultOptions() { + getDefaultOptions(): PreziOptions { return { width: 550, height: 400 }; } - extractVideoID(reference) { + extractVideoID(reference: string): string { let match = reference.match(/^https:\/\/prezi.com\/(.[^/]+)/); return match ? match[1] : reference; } - getVideoUrl(videoID) { + getVideoUrl(videoID: string): string { 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&" @@ -26,6 +29,3 @@ class PreziService extends VideoServiceBase { } } - - -module.exports = PreziService; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/SlideShareService.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/SlideShareService.js deleted file mode 100644 index d8dcbd5d65..0000000000 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/SlideShareService.js +++ /dev/null @@ -1,21 +0,0 @@ -"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; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/SlideShareService.ts b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/SlideShareService.ts new file mode 100644 index 0000000000..10e1b9506c --- /dev/null +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/SlideShareService.ts @@ -0,0 +1,22 @@ +import VideoServiceBase, { VideoServiceOptions } from './VideoServiceBase'; + +export interface SlideShareOptions extends VideoServiceOptions { + width?: number; + height?: number; +} + +export default class SlideShareService extends VideoServiceBase { + + getDefaultOptions(): SlideShareOptions { + return {width: 599, height: 487}; + } + + extractVideoID(reference: string): string { + return reference; + } + + getVideoUrl(videoID: string): string { + let escapedVideoID = this.env.md.utils.escapeHtml(videoID); + return `//www.slideshare.net/slideshow/embed_code/key/${escapedVideoID}`; + } +} diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VideoServiceBase.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VideoServiceBase.ts similarity index 59% rename from packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VideoServiceBase.js rename to packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VideoServiceBase.ts index 488c1d39f8..4111c0c5d8 100644 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VideoServiceBase.js +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VideoServiceBase.ts @@ -1,44 +1,61 @@ // 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"; +import PluginEnvironment from '../PluginEnvironment'; - -function defaultUrlFilter(url, _videoID, _serviceName, _options) { - return url; +export interface VideoServiceOptions { + width?: number; + height?: number; + ignoreStyle?: boolean; + [key: string]: any; } +export type UrlFilterDelegate = ( + url: string, + serviceName: string, + videoID: string, + options: any +) => string; + +const defaultUrlFilter: UrlFilterDelegate = (url) => url; -class VideoServiceBase { +abstract class VideoServiceBase { + public name: string; + public options: VideoServiceOptions; + public env: PluginEnvironment; - constructor(name, options, env) { + constructor(name: string, options: VideoServiceOptions, env: PluginEnvironment) { this.name = name; this.options = Object.assign(this.getDefaultOptions(), options); this.env = env; } - getDefaultOptions() { + public getDefaultOptions(): VideoServiceOptions { return {}; } - - extractVideoID(reference) { + + /** + * Overridden by child classes to extract the ID from a URL/reference. + */ + public extractVideoID(reference: string): string | null { return reference; } - getVideoUrl(_videoID) { - throw new Error("not implemented"); - } + /** + * Overridden by child classes to provide the specific embed URL. + */ + public abstract getVideoUrl(videoID: string): string; - getFilteredVideoUrl(videoID) { - let filterUrlDelegate = typeof this.env.options.filterUrl === "function" - ? this.env.options.filterUrl + public getFilteredVideoUrl(videoID: string): string { + let filterUrlDelegate: UrlFilterDelegate = typeof this.env.options.filterUrl === 'function' + ? (this.env.options.filterUrl as UrlFilterDelegate) : defaultUrlFilter; let videoUrl = this.getVideoUrl(videoID); return filterUrlDelegate(videoUrl, this.name, videoID, this.env.options); } - getEmbedCode(videoID) { - let containerClassNames = []; + public getEmbedCode(videoID: string): string { + let containerClassNames: string[] = []; if (this.env.options.containerClassName && this.options.ignoreStyle !== true) { containerClassNames.push(this.env.options.containerClassName); } @@ -46,9 +63,9 @@ class VideoServiceBase { let escapedServiceName = this.env.md.utils.escapeHtml(this.name); containerClassNames.push(this.env.options.serviceClassPrefix + escapedServiceName); - let containerStyles = []; + const containerStyles: string[] = []; if (this.options.ignoreStyle !== true) { - containerStyles.push(["position: relative;"]); + containerStyles.push('position: relative;'); if (this.options.height !== undefined && this.options.width !== undefined) { containerStyles.push(`padding-bottom: ${this.options.height / this.options.width * 100}%`); } @@ -75,19 +92,14 @@ class VideoServiceBase { } let iframeAttributes = iframeAttributeList - .map(pair => - pair[1] !== undefined - ? `${pair[0]}="${pair[1]}"` - : pair[0] - ) + .map(([key, value]) => (value !== undefined ? `${key}="${value}"` : key)) .join(" "); return `
` + `` - + `
\n`; + + `\n`; } } - -module.exports = VideoServiceBase; +export default VideoServiceBase; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VimeoService.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VimeoService.ts similarity index 62% rename from packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VimeoService.js rename to packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VimeoService.ts index 3a988c87d1..46bd4e8b1b 100644 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VimeoService.js +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VimeoService.ts @@ -1,28 +1,27 @@ // 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"); +import VideoServiceBase, { VideoServiceOptions } from "./VideoServiceBase"; +interface VimeoOptions extends VideoServiceOptions { + width?: number; + height?: number; +} -class VimeoService extends VideoServiceBase { +export default class VimeoService extends VideoServiceBase { - getDefaultOptions() { + getDefaultOptions(): VimeoOptions { return { width: 500, height: 281 }; } - extractVideoID(reference) { + extractVideoID(reference: string): string { 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) { + getVideoUrl(videoID: string): string { let escapedVideoID = this.env.md.utils.escapeHtml(videoID); return `//player.vimeo.com/video/${escapedVideoID}`; } } - - -module.exports = VimeoService; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VineService.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VineService.ts similarity index 59% rename from packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VineService.js rename to packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VineService.ts index d5d54c38b0..dd2d6b1de3 100644 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VineService.js +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/VineService.ts @@ -1,23 +1,29 @@ // 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"); - +import VideoServiceBase, { VideoServiceOptions } from './VideoServiceBase'; + +export interface VineOptions extends VideoServiceOptions { + width?: number; + height?: number; + /** + * The type of vine embed (e.g., 'simple'). + */ + embed?: string; +} -class VineService extends VideoServiceBase { +export default class VineService extends VideoServiceBase { - getDefaultOptions() { + getDefaultOptions(): VineOptions { return { width: 600, height: 600, embed: "simple" }; } - extractVideoID(reference) { + extractVideoID(reference: string): string { 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) { + getVideoUrl(videoID: string): string { 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}`; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/YouTubeService.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/YouTubeService.ts similarity index 56% rename from packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/YouTubeService.js rename to packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/YouTubeService.ts index 68a23a8376..7c01ff2775 100644 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/YouTubeService.js +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/services/YouTubeService.ts @@ -1,28 +1,28 @@ // 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"); +import VideoServiceBase, { VideoServiceOptions } from "./VideoServiceBase"; +export interface YouTubeOptions extends VideoServiceOptions { + width?: number; + height?: number; + [key: string]: any; +} -class YouTubeService extends VideoServiceBase { +export default class YouTubeService extends VideoServiceBase { - getDefaultOptions() { + getDefaultOptions(): YouTubeOptions { return { width: 640, height: 390 }; } - extractVideoID(reference) { + extractVideoID(reference: string): string { let match = reference.match(/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&\?]*).*/); return match && match[7].length === 11 ? match[7] : reference; } - getVideoUrl(videoID) { + getVideoUrl(videoID: string): string { let escapedVideoID = this.env.md.utils.escapeHtml(videoID); return `//www.youtube.com/embed/${escapedVideoID}`; } } - - -module.exports = YouTubeService; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/tokenizer.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/tokenizer.js deleted file mode 100644 index 7d5893863d..0000000000 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/tokenizer.js +++ /dev/null @@ -1,110 +0,0 @@ -// 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; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/tokenizer.ts b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/tokenizer.ts new file mode 100644 index 0000000000..233a2094a3 --- /dev/null +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-block-embed/tokenizer.ts @@ -0,0 +1,129 @@ +// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors +// Licensed under the MIT license. See LICENSE file in the project root. + +import StateBlock from 'markdown-it/lib/rules_block/state_block'; + +/** + * Interface for the service configuration used by the tokenizer. + */ +export interface EmbedService { + extractVideoID: (reference: string) => string | null; + [key: string]: any; +} + +export interface EmbedServiceMap { + [key: string]: EmbedService; +} + +interface Pointer { + line: number; + pos: number; + prevPos?: number; +} + +const SYNTAX_CHARS = ['@', '[', ']', '(', ')']; +const SYNTAX_CODES = SYNTAX_CHARS.map(char => char.charCodeAt(0)); + +/** + * Advances the pointer to the next occurrence of a specific symbol. + */ +function advanceToSymbol(state: StateBlock, endLine: number, symbol: string, pointer: Pointer): boolean { + let symbolLine = pointer.line; + let symbolIndex = state.src.indexOf(symbol, pointer.pos); + + if (symbolIndex === -1) return false; + + let 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; +} + +/** + * Factory function to create the tokenizer with access to configured services. + */ +export function createTokenizer(services: EmbedServiceMap) { + return function tokenizer(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean { + let startPos = state.bMarks[startLine] + state.tShift[startLine]; + let maxPos = state.eMarks[startLine]; + + let pointer: 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 + .substring(pointer.prevPos!, pointer.pos) + .trim() + .toLowerCase(); + + pointer.pos++; + + // Lookup service; if unknown, then this is not a known embed! + const service = services[serviceName]; + if (!service) return false; + + // Read embed reference from within parenthesis: (reference) + if (SYNTAX_CODES[3] !== state.src.charCodeAt(pointer.pos++)) return false; + if (!advanceToSymbol(state, endLine, ')', pointer)) return false; + + let videoReference = state.src + .substring(pointer.prevPos!, pointer.pos) + .trim(); + + pointer.pos++; + + // Do not recognize as block element when there is trailing text. + maxPos = state.eMarks[pointer.line]; + let trailingText = state.src + .substring(pointer.pos, maxPos) + .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) { + const nextLineStartPos = state.bMarks[pointer.line + 1] + state.tShift[pointer.line + 1]; + const nextLineMaxPos = state.eMarks[pointer.line + 1]; + if (nextLineMaxPos > nextLineStartPos) return false; + } + + if (pointer.line >= endLine) return false; + + if (!silent) { + const token = state.push('video', 'div', 0); + token.markup = state.src.slice(startPos, pointer.pos); + token.block = true; + (token as any).info = { + serviceName, + service, + videoReference, + videoID: service.extractVideoID(videoReference), + }; + token.map = [startLine, pointer.line + 1]; + + state.line = pointer.line + 1; + } + + return true; + }; +} diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-center-text.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-center-text.ts similarity index 58% rename from packages/core/src/lib/markdown-it/plugins/markdown-it-center-text.js rename to packages/core/src/lib/markdown-it/plugins/markdown-it-center-text.ts index 541fe80b39..23e5d6ba14 100644 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-center-text.js +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-center-text.ts @@ -25,48 +25,52 @@ // Process -> center text <- -'use strict'; +import MarkdownIt from 'markdown-it'; +import StateInline from 'markdown-it/lib/rules_inline/state_inline'; -module.exports = function centertext_plugin(md) { +/** + * A markdown-it plugin to center text using the syntax ->text<- + */ +export function centertext_plugin(md: MarkdownIt): void { + function tokenize(state: StateInline, silent: boolean): boolean { + let token; + const max = state.posMax; + const start = state.pos; + const marker = state.src.charCodeAt(start); - function tokenize(state, silent) { - var token, - max = state.posMax, - start = state.pos, - marker = state.src.charCodeAt(start); if (start + 1 > max) { return false; } if (silent) { return false; } // don't run any pairs in validation mode if (marker === 45/* - */ && state.src.charCodeAt(start + 1) === 62/* > */ - ) { + ) { state.scanDelims(state.pos, true); - token = state.push('text', '', 0); + token = state.push('text', '', 0); token.content = '->'; state.delimiters.push({ - marker: token.content, - jump: 0, - token: state.tokens.length - 1, - level: state.level, - end: -1, - open: true, - close: false + marker: 45, // CHANGED: Use character code 45 instead of string '->' + jump: 0, + token: state.tokens.length - 1, + length: 2, + end: -1, + open: true, + close: false, }); } else if (marker === 60/* < */ && state.src.charCodeAt(start + 1) === 45/* - */ - ) { + ) { // found the close marker state.scanDelims(state.pos, true); - token = state.push('text', '', 0); + token = state.push('text', '', 0); token.content = '<-'; state.delimiters.push({ - marker: token.content, - jump: 0, - token: state.tokens.length - 1, - level: state.level, - end: -1, - open: false, - close: true + marker: 60, // CHANGED: Use character code 60 instead of string '<-' + jump: 0, + token: state.tokens.length - 1, + length: 2, + end: -1, + open: false, + close: true, }); } else { // neither @@ -78,23 +82,21 @@ module.exports = function centertext_plugin(md) { return true; } - // Walk through delimiter list and replace text tokens with tags - // - function postProcess(state) { - var i, - foundStart = false, - foundEnd = false, - delim, - token, - delimiters = state.delimiters, - max = state.delimiters.length; + function postProcess(state: StateInline): boolean { + let i; + let foundStart = false; + let foundEnd = false; + let delim; + let token; + const delimiters = state.delimiters; + const max = state.delimiters.length; for (i = 0; i < max; i++) { delim = delimiters[i]; - if (delim.marker === '->') { + if (delim.marker === 45/* - */) { foundStart = true; - } else if (delim.marker === '<-') { + } else if (delim.marker === 60/* < */) { foundEnd = true; } } @@ -102,29 +104,30 @@ module.exports = function centertext_plugin(md) { for (i = 0; i < max; i++) { delim = delimiters[i]; - if (delim.marker === '->') { + if (delim.marker === 45/* - */) { foundStart = true; - token = state.tokens[delim.token]; - token.type = 'centertext_open'; - token.tag = 'div'; + token = state.tokens[delim.token]; + token.type = 'centertext_open'; + token.tag = 'div'; token.nesting = 1; - token.markup = '->'; + token.markup = '->'; token.content = ''; - token.attrs = [ [ 'class', 'text-center' ] ]; - } else if (delim.marker === '<-') { + token.attrs = [['class', 'text-center']]; + } else if (delim.marker === 60/* < */) { if (foundStart) { - token = state.tokens[delim.token]; - token.type = 'centertext_close'; - token.tag = 'div'; + token = state.tokens[delim.token]; + token.type = 'centertext_close'; + token.tag = 'div'; token.nesting = -1; - token.markup = '<-'; + token.markup = '<-'; token.content = ''; } } } } + return true; } md.inline.ruler.before('emphasis', 'centertext', tokenize); md.inline.ruler2.before('emphasis', 'centertext', postProcess); -}; +} diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-colour-text.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-colour-text.js deleted file mode 100644 index ba1081c5bf..0000000000 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-colour-text.js +++ /dev/null @@ -1,151 +0,0 @@ -/* - Copyright (c) 2016-2018 Jay Hodgson - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. - */ - -// Process #(char)# coloured text ## - -'use strict'; - -module.exports = function colourtext_plugin(md) { - const acsiiCodeToTokenMap = new Map([ - [114, '#r#'], - [103, '#g#'], - [98, '#b#'], - [99, '#c#'], - [109, '#m#'], - [121, '#y#'], - [107, '#k#'], - [119, '#w#'], - ]); - const delimMarkerToClassMap = new Map([ - ['#r#', 'mkb-text-red'], - ['#g#', 'mkb-text-green'], - ['#b#', 'mkb-text-blue'], - ['#c#', 'mkb-text-cyan'], - ['#m#', 'mkb-text-magenta'], - ['#y#', 'mkb-text-yellow'], - ['#k#', 'mkb-text-black'], - ['#w#', 'mkb-text-white'], - ]); - - function tokenize(state, silent) { - var token, - max = state.posMax, - start = state.pos, - marker = state.src.charCodeAt(start); - if (start + 1 > max) { return false; } - if (silent) { return false; } // don't run any pairs in validation mode - - if (marker === 35/* # */ && - acsiiCodeToTokenMap.has(state.src.charCodeAt(start + 1)) && - start + 2 <= max && - state.src.charCodeAt(start + 2) === 35/* # */ - ) { - state.scanDelims(state.pos, true); - token = state.push('text', '', 0); - token.content = acsiiCodeToTokenMap.get(state.src.charCodeAt(start + 1)); - state.delimiters.push({ - marker: token.content, - jump: 0, - token: state.tokens.length - 1, - level: state.level, - end: -1, - open: true, - close: false - }); - state.pos += 3; - } else if (marker === 35/* # */ && - state.src.charCodeAt(start + 1) === 35/* # */ - ) { - // found the close marker - state.scanDelims(state.pos, true); - token = state.push('text', '', 0); - token.content = '##'; - state.delimiters.push({ - marker: token.content, - jump: 0, - token: state.tokens.length - 1, - level: state.level, - end: -1, - open: false, - close: true - }); - state.pos += 2; - } else { - // neither - return false; - } - - return true; - } - - - // Walk through delimiter list and replace text tokens with tags - function postProcess(state) { - var i, - foundStart = false, - foundEnd = false, - delim, - token, - delimiters = state.delimiters, - max = state.delimiters.length; - - for (i = 0; i < max; i++) { - delim = delimiters[i]; - if (delimMarkerToClassMap.has(delim.marker)) { - foundStart = true; - } else if (delim.marker === '##') { - foundEnd = true; - } - } - if (foundStart && foundEnd) { - for (i = 0; i < max; i++) { - delim = delimiters[i]; - - if (delimMarkerToClassMap.has(delim.marker)) { - foundStart = true; - token = state.tokens[delim.token]; - token.type = 'colourtext_open'; - token.tag = 'span'; - token.nesting = 1; - token.markup = delim.marker; - token.content = ''; - token.attrs = [ [ 'class', delimMarkerToClassMap.get(delim.marker) ] ]; - } else if (delim.marker === '##') { - if (foundStart) { - token = state.tokens[delim.token]; - token.type = 'colourtext_close'; - token.tag = 'span'; - token.nesting = -1; - token.markup = '##'; - token.content = ''; - } - } - } - } - } - - md.inline.ruler.before('strikethrough', 'colourtext', tokenize); - md.inline.ruler2.before('strikethrough', 'colourtext', postProcess); -}; diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-colour-text.ts b/packages/core/src/lib/markdown-it/plugins/markdown-it-colour-text.ts new file mode 100644 index 0000000000..3c625894a3 --- /dev/null +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-colour-text.ts @@ -0,0 +1,158 @@ +/* + Copyright (c) 2016-2018 Jay Hodgson + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + */ + +// Process #(char)# coloured text ## + +import MarkdownIt from 'markdown-it'; +import StateInline from 'markdown-it/lib/rules_inline/state_inline'; + +export function colourTextPlugin(md: MarkdownIt): void { + const asciiCodeToTokenMap = new Map([ + [114, '#r#'], + [103, '#g#'], + [98, '#b#'], + [99, '#c#'], + [109, '#m#'], + [121, '#y#'], + [107, '#k#'], + [119, '#w#'], + ]); + + const delimMarkerToClassMap = new Map([ + ['#r#', 'mkb-text-red'], + ['#g#', 'mkb-text-green'], + ['#b#', 'mkb-text-blue'], + ['#c#', 'mkb-text-cyan'], + ['#m#', 'mkb-text-magenta'], + ['#y#', 'mkb-text-yellow'], + ['#k#', 'mkb-text-black'], + ['#w#', 'mkb-text-white'], + ]); + + function tokenize(state: StateInline, silent: boolean): boolean { + let token; + const max = state.posMax; + const start = state.pos; + const marker = state.src.charCodeAt(start); + + if (start + 1 > max) { return false; } + if (silent) { return false; } // don't run any pairs in validation mode + + const nextChar = state.src.charCodeAt(start + 1); + + // Check for opening: #x# + if (marker === 35/* # */ && + asciiCodeToTokenMap.has(nextChar) && + start + 2 <= max && + state.src.charCodeAt(start + 2) === 35/* # */ + ) { + state.scanDelims(state.pos, true); + token = state.push('text', '', 0); + const tokenContent = asciiCodeToTokenMap.get(nextChar)!; + token.content = tokenContent; + + state.delimiters.push({ + marker: 35, // Use # as the marker code + length: 3, // length of #x# is 3 + jump: 0, + token: state.tokens.length - 1, + end: -1, + open: true, + close: false, + }); + state.pos += 3; + } + // Check for closing: ## + else if (marker === 35/* # */ && nextChar === 35/* # */) { + state.scanDelims(state.pos, true); + token = state.push('text', '', 0); + token.content = '##'; + + state.delimiters.push({ + marker: 35, + length: 2, // length of ## is 2 + jump: 0, + token: state.tokens.length - 1, + end: -1, + open: false, + close: true, + }); + state.pos += 2; + } else { + return false; + } + + return true; + } + + function postProcess(state: StateInline): boolean { + let foundStart = false; + let foundEnd = false; + const { delimiters, tokens } = state; + + for (let i = 0; i < delimiters.length; i++) { + const delim = delimiters[i]; + if (delim.marker === 35 && delim.open) { + if (delimMarkerToClassMap.has(tokens[delim.token].content)) { + foundStart = true; + } + } else if (delim.marker === 35 && delim.close) { + foundEnd = true; + } + } + + if (foundStart && foundEnd) { + foundStart = false; + for (let i = 0; i < delimiters.length; i++) { + const delim = delimiters[i]; + const token = tokens[delim.token]; + + if (delim.marker === 35 && delim.open) { + // Check if the content is one of our color tokens + const className = delimMarkerToClassMap.get(token.content); + if (className) { + foundStart = true; + token.type = 'colourtext_open'; + token.tag = 'span'; + token.nesting = 1; + token.markup = token.content; + token.content = ''; + token.attrs = [['class', className]]; + } + } else if (delim.marker === 35 && delim.close) { + token.type = 'colourtext_close'; + token.tag = 'span'; + token.nesting = -1; + token.markup = '##'; + token.content = ''; + } + } + } + return true; + } + + md.inline.ruler.before('strikethrough', 'colourtext', tokenize); + md.inline.ruler2.before('strikethrough', 'colourtext', postProcess); +} diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-double-delimiter.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-double-delimiter.ts similarity index 71% rename from packages/core/src/lib/markdown-it/plugins/markdown-it-double-delimiter.js rename to packages/core/src/lib/markdown-it/plugins/markdown-it-double-delimiter.ts index e171048598..0a1820d42c 100644 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-double-delimiter.js +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-double-delimiter.ts @@ -1,5 +1,3 @@ -'use strict'; - /* Copyright (c) 2014-2015 Vitaly Puzrin, Alex Kocharin @@ -31,6 +29,9 @@ - create new inline rules dynamically for different delimiters */ +import MarkdownIt from 'markdown-it'; +import StateInline from 'markdown-it/lib/rules_inline/state_inline'; + /** * Creates a new inline rule for markdown-it, specifically for rules that uses a 'double' delimiter. * Some examples are ++, --, !!, %%. @@ -41,45 +42,47 @@ * link for the existing inline rules in markdown-it. * https://github.com/markdown-it/markdown-it/blob/master/lib/parser_inline.js */ -module.exports = function createDoubleDelimiterInlineRule(delimiter, ruleName, nextRule) { - const doubleDelimiterPlugin = function double_delimiter_plugin(md) { +export function createDoubleDelimiterInlineRule(delimiter: string, ruleName: string, nextRule: string) { + return function double_delimiter_plugin(md: MarkdownIt): void { + // 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); + function tokenize(state: StateInline, silent: boolean): boolean { + let token; + const start = state.pos; + const marker = state.src.charCodeAt(start); - if (silent) { - return false; - } + if (silent) { return false; } + // Check if the current character matches our specific delimiter (e.g., '!', '+', etc.) if (marker !== delimiter.charCodeAt(0)) { return false; } - scanned = state.scanDelims(state.pos, true); - len = scanned.length; - ch = String.fromCharCode(marker); + const scanned = state.scanDelims(state.pos, true); + let len = scanned.length; + const ch = String.fromCharCode(marker); if (len < 2) { return false; } + // If we have an odd number of delimiters (like +++), + // treat the first one as plain text and use the rest as pairs. if (len % 2) { token = state.push('text', '', 0); token.content = ch; len--; } - for (i = 0; i < len; i += 2) { + for (let i = 0; i < len; i += 2) { token = state.push('text', '', 0); token.content = ch + ch; state.delimiters.push({ marker: marker, - jump: i, + length: 2, // double-delimiter rule, so length is always 2 + jump: i / 2, // track position in the sequence token: state.tokens.length - 1, - level: state.level, end: -1, open: scanned.can_open, close: scanned.can_close @@ -87,36 +90,30 @@ module.exports = function createDoubleDelimiterInlineRule(delimiter, ruleName, n } 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; + function postProcess(state: StateInline): boolean { + let i, j; + let startDelim; + let endDelim; + let token; + const loneMarkers: number[] = []; + const delimiters = state.delimiters; + const max = state.delimiters.length; for (i = 0; i < max; i++) { startDelim = delimiters[i]; - if (startDelim.marker !== delimiter.charCodeAt(0)) { - continue; - } - - if (startDelim.end === -1) { - continue; - } + if (startDelim.marker !== delimiter.charCodeAt(0)) continue; + if (startDelim.end === -1) continue; endDelim = delimiters[startDelim.end]; + // LOGIC PRESERVATION: Convert the 'text' tokens into open/close spans token = state.tokens[startDelim.token]; - token.type = `${ruleName}_open` + token.type = `${ruleName}_open`; token.tag = 'span'; token.attrs = [['class', ruleName]]; token.nesting = 1; @@ -124,15 +121,15 @@ module.exports = function createDoubleDelimiterInlineRule(delimiter, ruleName, n token.content = ''; token = state.tokens[endDelim.token]; - token.type = `${ruleName}_close` + token.type = `${ruleName}_close`; token.tag = 'span'; token.nesting = -1; token.markup = delimiter; token.content = ''; + // Handle the "lone marker" edge case (odd number of markers) if (state.tokens[endDelim.token - 1].type === 'text' && state.tokens[endDelim.token - 1].content === delimiter.charAt(0)) { - loneMarkers.push(endDelim.token - 1); } } @@ -143,7 +140,7 @@ module.exports = function createDoubleDelimiterInlineRule(delimiter, ruleName, n // // So, we have to move all those markers after subsequent s_close tags. while (loneMarkers.length) { - i = loneMarkers.pop(); + i = loneMarkers.pop()!; j = i + 1; while (j < state.tokens.length && state.tokens[j].type === `${ruleName}_close`) { @@ -158,11 +155,10 @@ module.exports = function createDoubleDelimiterInlineRule(delimiter, ruleName, n state.tokens[i] = token; } } + return true; } md.inline.ruler.before(nextRule, ruleName, tokenize); md.inline.ruler2.before(nextRule, ruleName, postProcess); - } - - return doubleDelimiterPlugin; -}; + }; +} diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-footnotes.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-footnotes.ts similarity index 71% rename from packages/core/src/lib/markdown-it/plugins/markdown-it-footnotes.js rename to packages/core/src/lib/markdown-it/plugins/markdown-it-footnotes.ts index ab192a6682..b7ae688b01 100644 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-footnotes.js +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-footnotes.ts @@ -32,56 +32,56 @@ Changes are delimited with a // CHANGE HERE comment */ -const { MARKBIND_FOOTNOTE_POPOVER_ID_PREFIX } = require('../../../html/constants') +import MarkdownIt from 'markdown-it'; +import Renderer from 'markdown-it/lib/renderer'; +import Token from 'markdown-it/lib/token'; +import StateBlock from 'markdown-it/lib/rules_block/state_block'; +import StateInline from 'markdown-it/lib/rules_inline/state_inline'; +import StateCore from 'markdown-it/lib/rules_core/state_core'; +import { MARKBIND_FOOTNOTE_POPOVER_ID_PREFIX } from '../../../html/constants'; // Process footnotes // -'use strict'; //////////////////////////////////////////////////////////////////////////////// // Renderer partials -function render_footnote_anchor_name(tokens, idx, options, env/*, slf*/) { - var n = Number(tokens[idx].meta.id + 1).toString(); - var prefix = ''; +function render_footnote_anchor_name(tokens: Token[], idx: number, _options: MarkdownIt.Options, env: any) { + const n = Number(tokens[idx].meta.id + 1).toString(); + let prefix = ''; if (typeof env.docId === 'string') { - prefix = '-' + env.docId + '-'; + prefix = `-${env.docId}-`; } return prefix + n; } -function render_footnote_caption(tokens, idx/*, options, env, slf*/) { - var n = Number(tokens[idx].meta.id + 1).toString(); +function render_footnote_caption(tokens: Token[], idx: number) { + let n = Number(tokens[idx].meta.id + 1).toString(); if (tokens[idx].meta.subId > 0) { - n += ':' + tokens[idx].meta.subId; + n += `:${tokens[idx].meta.subId}`; } - return '[' + n + ']'; + return `[${n}]`; } -function render_footnote_ref(tokens, idx, options, env, slf) { - var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf); - var caption = slf.rules.footnote_caption(tokens, idx, options, env, slf); - var refid = id; - - if (tokens[idx].meta.subId > 0) { - refid += ':' + tokens[idx].meta.subId; - } +function render_footnote_ref(tokens: Token[], idx: number, options: MarkdownIt.Options, env: any, slf: Renderer) { + const id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf); + const caption = slf.rules.footnote_caption!(tokens, idx, options, env, slf); // CHANGE here // Old code: // return '' + caption + ''; // Additions: for footnote popover and aria-describedby label - return '' - + '' + return `` + + `` + caption - + '' + + ''; } -function render_footnote_block_open(tokens, idx, options) { +function render_footnote_block_open() { // CHANGE HERE - change this into something easily recognisable and combinable by {@link NodeProcessor} // Original: /*return (options.xhtmlOut ? '
\n' : '
\n') + @@ -97,27 +97,21 @@ function render_footnote_block_close() { return '\n'; } -function render_footnote_open(tokens, idx, options, env, slf) { - var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf); +function render_footnote_open(tokens: Token[], idx: number, options: MarkdownIt.Options, env: any, slf: Renderer) { + let id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf); if (tokens[idx].meta.subId > 0) { - id += ':' + tokens[idx].meta.subId; + id += `:${tokens[idx].meta.subId}`; } - return '
  • '; + return `
  • `; } function render_footnote_close() { return '
  • \n'; } -function render_footnote_anchor(tokens, idx, options, env, slf) { - var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf); - - if (tokens[idx].meta.subId > 0) { - id += ':' + tokens[idx].meta.subId; - } - +function render_footnote_anchor() { // CHANGE HERE // Below line adds backreferences, but doesn't work well with panels, so disabled for now. // Old code: @@ -127,10 +121,9 @@ function render_footnote_anchor(tokens, idx, options, env, slf) { return ''; } - -module.exports = function footnote_plugin(md) { - var parseLinkLabel = md.helpers.parseLinkLabel, - isSpace = md.utils.isSpace; +export function footnotePlugin(md: MarkdownIt): void { + const parseLinkLabel = md.helpers.parseLinkLabel; + const isSpace = md.utils.isSpace; md.renderer.rules.footnote_ref = render_footnote_ref; md.renderer.rules.footnote_block_open = render_footnote_block_open; @@ -144,11 +137,13 @@ module.exports = function footnote_plugin(md) { md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name; // Process footnote block definition - function footnote_def(state, startLine, endLine, silent) { - var oldBMark, oldTShift, oldSCount, oldParentType, pos, label, token, - initial, offset, ch, posAfterColon, - start = state.bMarks[startLine] + state.tShift[startLine], - max = state.eMarks[startLine]; + function footnote_def(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean { + let pos; + let label; + let token; + let ch; + 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; } @@ -158,7 +153,7 @@ module.exports = function footnote_plugin(md) { for (pos = start + 2; pos < max; pos++) { if (state.src.charCodeAt(pos) === 0x20) { return false; } - if (state.src.charCodeAt(pos) === 0x5D /* ] */) { + if (state.src.charCodeAt(pos) === 0x5D /* ] */) { break; } } @@ -171,20 +166,21 @@ module.exports = function footnote_plugin(md) { 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; + state.env.footnotes.refs[`:${label}`] = -1; token = new state.Token('footnote_reference_open', '', 1); token.meta = { label: label }; token.level = state.level++; state.tokens.push(token); - oldBMark = state.bMarks[startLine]; - oldTShift = state.tShift[startLine]; - oldSCount = state.sCount[startLine]; - oldParentType = state.parentType; + const oldBMark = state.bMarks[startLine]; + const oldTShift = state.tShift[startLine]; + const oldSCount = state.sCount[startLine]; + const oldParentType = state.parentType; - posAfterColon = pos; - initial = offset = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]); + const posAfterColon = pos; + const initial = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]); + let offset = initial; while (pos < max) { ch = state.src.charCodeAt(pos); @@ -207,13 +203,13 @@ module.exports = function footnote_plugin(md) { state.bMarks[startLine] = posAfterColon; state.blkIndent += 4; - state.parentType = 'footnote'; + state.parentType = 'footnote' as any; // Note find a better fix for this. if (state.sCount[startLine] < state.blkIndent) { state.sCount[startLine] += state.blkIndent; } - state.md.block.tokenize(state, startLine, endLine, true); + state.md.block.tokenize(state, startLine, endLine); state.parentType = oldParentType; state.blkIndent -= 4; @@ -229,14 +225,14 @@ module.exports = function footnote_plugin(md) { } // Process inline footnotes (^[...]) - function footnote_inline(state, silent) { - var labelStart, - labelEnd, - footnoteId, - token, - tokens, - max = state.posMax, - start = state.pos; + function footnote_inline(state: StateInline, silent: boolean): boolean { + let labelStart; + let labelEnd; + let footnoteId; + let token; + let tokens: Token[]; + const max = state.posMax; + const start = state.pos; if (start + 2 >= max) { return false; } if (state.src.charCodeAt(start) !== 0x5E/* ^ */) { return false; } @@ -278,18 +274,18 @@ module.exports = function footnote_plugin(md) { } // Process footnote references ([^...]) - function footnote_ref(state, silent) { - var label, - pos, - footnoteId, - footnoteSubId, - token, - max = state.posMax, - start = state.pos; + function footnote_ref(state: StateInline, silent: boolean): boolean { + 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; } @@ -302,29 +298,28 @@ module.exports = function footnote_plugin(md) { } } - if (pos === start + 2) { return false; } // no empty footnote labels - if (pos >= max) { return false; } + if (pos === start + 2 || pos >= max) { return false; } // no empty footnote labels pos++; label = state.src.slice(start + 2, pos - 1); - if (typeof state.env.footnotes.refs[':' + label] === 'undefined') { return false; } + 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) { + if (state.env.footnotes.refs[`:${label}`] < 0) { footnoteId = state.env.footnotes.list.length; state.env.footnotes.list[footnoteId] = { label: label, count: 0 }; - state.env.footnotes.refs[':' + label] = footnoteId; + state.env.footnotes.refs[`:${label}`] = footnoteId; } else { - footnoteId = state.env.footnotes.refs[':' + label]; + 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: label }; + token.meta = { id: footnoteId, subId: footnoteSubId, label }; } state.pos = pos; @@ -333,14 +328,14 @@ module.exports = function footnote_plugin(md) { } // Glue footnote tokens to end of token stream - function footnote_tail(state) { - var i, l, j, t, lastParagraph, list, token, tokens, current, currentLabel, - insideRef = false, - refTokens = {}; + function footnote_tail(state: StateCore): void { + let i, l, j, t, lastParagraph, list, token, tokens: Token[], current: Token[] = [], currentLabel: string = ''; + let insideRef = false; + const refTokens: { [key: string]: Token[] } = {}; if (!state.env.footnotes) { return; } - state.tokens = state.tokens.filter(function (tok) { + state.tokens = state.tokens.filter((tok) => { if (tok.type === 'footnote_reference_open') { insideRef = true; current = []; @@ -349,8 +344,7 @@ module.exports = function footnote_plugin(md) { } if (tok.type === 'footnote_reference_close') { insideRef = false; - // prepend ':' to avoid conflict with Object.prototype members - refTokens[':' + currentLabel] = current; + refTokens[`:${currentLabel}`] = current; return false; } if (insideRef) { current.push(tok); } @@ -384,13 +378,16 @@ module.exports = function footnote_plugin(md) { token.block = true; tokens.push(token); - } else if (list[i].label) { - tokens = refTokens[':' + list[i].label]; + } else if (list[i].label && refTokens[`:${list[i].label}`]) { + tokens = refTokens[`:${list[i].label}`]; + } else { + tokens = []; } state.tokens = state.tokens.concat(tokens); - if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') { - lastParagraph = state.tokens.pop(); + const lastToken = state.tokens[state.tokens.length - 1]; + if (lastToken && lastToken.type === 'paragraph_close') { + lastParagraph = state.tokens.pop()!; } else { lastParagraph = null; } @@ -418,4 +415,4 @@ module.exports = function footnote_plugin(md) { 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); -}; +} diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-icons.ts b/packages/core/src/lib/markdown-it/plugins/markdown-it-icons.ts index 00e5cccbba..1c6b92b4c1 100644 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-icons.ts +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-icons.ts @@ -1,7 +1,9 @@ // import the necessary packages -const octicons = require('@primer/octicons'); -const cheerio = require('cheerio'); -const markdownItRegExp = require('markdown-it-regexp'); +import octicons from '@primer/octicons'; +import * as cheerio from 'cheerio'; +// markdown-it-regexp type definitions are undefined +// @ts-ignore +import markdownItRegExp from 'markdown-it-regexp'; // regular expression to match the icon patterns const ICON_REGEXP @@ -9,6 +11,8 @@ const ICON_REGEXP // function to get the octicon icons function getOcticonIcon(iconName: string) { + // Indexing octicons varies based on @types/primer__octicons version + // @ts-ignore return octicons[iconName] ?? null; } diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-radio-button.js b/packages/core/src/lib/markdown-it/plugins/markdown-it-radio-button.js deleted file mode 100644 index 1af8b3b11c..0000000000 --- a/packages/core/src/lib/markdown-it/plugins/markdown-it-radio-button.js +++ /dev/null @@ -1,126 +0,0 @@ -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 { - var hash = crypto.createHash('md5'); - if (i >= 5 && tokens[i-5]) { - hash.update(tokens[i-5].content); - } - if (i >= 4 && tokens[i-4]) { - hash.update(tokens[i-4].content); - } - group = hash.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="" ' : ''; - - const isUnchecked = token.content.indexOf('( ) ') === 0; - const isChecked = token.content.indexOf('(x) ') === 0 || - token.content.indexOf('(X) ') === 0; - if (isUnchecked) { - radio.content = ''; - } else if (isChecked) { - radio.content = ''; - } - 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 = ''; - 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; -} diff --git a/packages/core/src/lib/markdown-it/plugins/markdown-it-radio-button.ts b/packages/core/src/lib/markdown-it/plugins/markdown-it-radio-button.ts new file mode 100644 index 0000000000..a0083b9755 --- /dev/null +++ b/packages/core/src/lib/markdown-it/plugins/markdown-it-radio-button.ts @@ -0,0 +1,128 @@ +import crypto from 'crypto'; +import MarkdownIt from 'markdown-it'; +import Token from 'markdown-it/lib/token'; +import StateCore from 'markdown-it/lib/rules_core/state_core'; + +interface RadioOptions { + enabled?: boolean; + label?: boolean; +} + +/** + * Modified from https://github.com/revin/markdown-it-task-lists/blob/master/index.js + */ +export function radioButtonPlugin(md: MarkdownIt, options?: RadioOptions): void { + const disableRadio = options ? !options.enabled : false; + const useLabelWrapper = options ? !!options.label : true; + + md.core.ruler.after('inline', 'radio-lists', (state: StateCore) => { + const tokens = state.tokens; + for (let i = 2; i < tokens.length; i++) { + if (isTodoItem(tokens, i)) { + const parentIdx = parentToken(tokens, i - 2); + if (parentIdx === -1) continue; + + const parent = tokens[parentIdx]; + let groupAttr = attrGet(parent, 'radio-group'); // try retrieve the group id + let group: string; + + if (groupAttr) { + group = groupAttr[1]; + } else { + const hash = crypto.createHash('md5'); + if (i >= 5 && tokens[i-5]) { + hash.update(tokens[i-5].content); + } + if (i >= 4 && tokens[i-4]) { + hash.update(tokens[i-4].content); + } + group = hash.update(tokens[i].content).digest('hex').slice(2, 7); // generate a deterministic group id + } + radioify(tokens[i], group, disableRadio, useLabelWrapper); + attrSet(tokens[i - 2], 'class', 'radio-list-item'); + attrSet(parent, 'radio-group', group); // save the group id to the top-level list + attrSet(parent, 'class', 'radio-list'); + } + } + }); +} + +function attrSet(token: Token, name: string, value: string): void { + const index = token.attrIndex(name); + const attr: [string, string] = [name, value]; + + if (index < 0) { + token.attrPush(attr); + } else { + if (token.attrs) { + token.attrs[index] = attr; + } + } +} + +function attrGet(token: Token, name: string): [string, string] | undefined { + const index = token.attrIndex(name); + + if (index < 0 || !token.attrs) { + return undefined; + } + return token.attrs[index] as [string, string]; +} + +function parentToken(tokens: Token[], index: number): number { + const targetLevel = tokens[index].level - 1; + for (let i = index - 1; i >= 0; i--) { + if (tokens[i].level === targetLevel) { + return i; + } + } + return -1; +} + +function isTodoItem(tokens: Token[], index: number): boolean { + return isInline(tokens[index]) && + isParagraph(tokens[index - 1]) && + isListItem(tokens[index - 2]) && + startsWithTodoMarkdown(tokens[index]); +} + +function radioify(token: Token, radioId: string, disableRadio: boolean, useLabelWrapper: boolean): void { + if (!token.children) token.children = []; + + token.children.unshift(makeRadioButton(token, radioId, disableRadio)); + token.children[1].content = token.children[1].content.slice(3); + token.content = token.content.slice(3); + + if (useLabelWrapper) { + // Removed beingLabel & endLabel functions since we can just use new Token(...) now. + token.children.unshift(new Token('html_inline', '', 0)); + token.children[0].content = ''; + } +} + +function makeRadioButton(token: Token, radioId: string, disableRadio: boolean): Token { + const radio = new Token('html_inline', '', 0); + const disabledAttr = disableRadio ? ' disabled="" ' : ''; + + const isUnchecked = token.content.indexOf('( ) ') === 0; + const isChecked = token.content.indexOf('(x) ') === 0 || + token.content.indexOf('(X) ') === 0; + if (isUnchecked) { + radio.content = ``; + } else if (isChecked) { + radio.content = ``; + } + return radio; +} + +function isInline(token: Token): boolean { return token.type === 'inline'; } +function isParagraph(token: Token): boolean { return token.type === 'paragraph_open'; } +function isListItem(token: Token): boolean { return token.type === 'list_item_open'; } + +function startsWithTodoMarkdown(token: Token): boolean { + // 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; +} diff --git a/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-block-embed.test.ts b/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-block-embed.test.ts index 5852a83500..a1c69d0beb 100644 --- a/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-block-embed.test.ts +++ b/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-block-embed.test.ts @@ -1,6 +1,6 @@ import markdownIt from 'markdown-it'; -const blockEmbedPlugin = require('../../../../../src/lib/markdown-it/plugins/markdown-it-block-embed'); +import blockEmbedPlugin from '../../../../../src/lib/markdown-it/plugins/markdown-it-block-embed'; describe('markdown-it-block-embed plugin', () => { let md: markdownIt; diff --git a/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-center-text.test.ts b/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-center-text.test.ts index 8217898824..ecb55d592f 100644 --- a/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-center-text.test.ts +++ b/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-center-text.test.ts @@ -1,13 +1,13 @@ import markdownIt from 'markdown-it'; -const centerTextPlugin = require('../../../../../src/lib/markdown-it/plugins/markdown-it-center-text'); +import { centertext_plugin } from '../../../../../src/lib/markdown-it/plugins/markdown-it-center-text'; describe('markdown-it-center-text plugin', () => { let md: markdownIt; beforeEach(() => { md = markdownIt(); - md.use(centerTextPlugin); + md.use(centertext_plugin); }); test('should render center text with various content types', () => { diff --git a/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-colour-text.test.ts b/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-colour-text.test.ts index 363476d7fa..da2c39aaaa 100644 --- a/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-colour-text.test.ts +++ b/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-colour-text.test.ts @@ -1,6 +1,6 @@ import markdownIt from 'markdown-it'; -const colourTextPlugin = require('../../../../../src/lib/markdown-it/plugins/markdown-it-colour-text'); +import { colourTextPlugin } from '../../../../../src/lib/markdown-it/plugins/markdown-it-colour-text'; describe('markdown-it-colour-text plugin', () => { let md: markdownIt; diff --git a/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-double-delimiter.test.ts b/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-double-delimiter.test.ts index 9ab53b448d..d6c22da0da 100644 --- a/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-double-delimiter.test.ts +++ b/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-double-delimiter.test.ts @@ -1,7 +1,8 @@ import markdownIt from 'markdown-it'; -const createDoubleDelimiterInlineRule - = require('../../../../../src/lib/markdown-it/plugins/markdown-it-double-delimiter'); +import { + createDoubleDelimiterInlineRule, +} from '../../../../../src/lib/markdown-it/plugins/markdown-it-double-delimiter'; describe('markdown-it-double-delimiter plugin', () => { let md: markdownIt; diff --git a/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-footnotes.test.ts b/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-footnotes.test.ts index 3a98cfffcc..1f383ac1b1 100644 --- a/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-footnotes.test.ts +++ b/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-footnotes.test.ts @@ -1,13 +1,13 @@ import markdownIt from 'markdown-it'; -const footnotesPlugin = require('../../../../../src/lib/markdown-it/plugins/markdown-it-footnotes'); +import { footnotePlugin } from '../../../../../src/lib/markdown-it/plugins/markdown-it-footnotes'; describe('markdown-it-footnotes plugin', () => { let md: markdownIt; beforeEach(() => { md = markdownIt(); - md.use(footnotesPlugin); + md.use(footnotePlugin); }); describe('basic footnotes', () => { @@ -209,7 +209,7 @@ describe('markdown-it-footnotes plugin', () => { test('should handle docId environment and subId scenarios', () => { // Test with docId in environment (covers env.docId branch at line 48) const docIdMd = markdownIt(); - docIdMd.use(footnotesPlugin); + docIdMd.use(footnotePlugin); const env = { docId: 'test-doc' }; const docIdSource = [ 'Text with footnote[^1].', diff --git a/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-radio-button.test.ts b/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-radio-button.test.ts index d4585c80d8..b6e9ba29e0 100644 --- a/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-radio-button.test.ts +++ b/packages/core/test/unit/lib/markdown-it/plugins/markdown-it-radio-button.test.ts @@ -1,7 +1,8 @@ import markdownIt from 'markdown-it'; +import { radioButtonPlugin } from '../../../../../src/lib/markdown-it/plugins/markdown-it-radio-button'; + const markdownItTaskLists = require('markdown-it-task-lists'); -const radioButtonPlugin = require('../../../../../src/lib/markdown-it/plugins/markdown-it-radio-button'); describe('markdown-it-radio-button plugin', () => { let md: markdownIt;