diff --git a/dist/defaults.js b/dist/defaults.js index 3679c67..868330e 100644 --- a/dist/defaults.js +++ b/dist/defaults.js @@ -42,6 +42,7 @@ exports.defaultTags = new tags.TagCollection([ new tags.Strikethrough(), new tags.Code(), new tags.Anchor(), + new tags.Image(), new tags.HardBreak(), new tags.FencedCode(), new tags.HorizontalRule(), diff --git a/dist/tags.d.ts b/dist/tags.d.ts deleted file mode 100644 index c3b97e6..0000000 --- a/dist/tags.d.ts +++ /dev/null @@ -1,232 +0,0 @@ -import type { Token, TokenRole, TagType, TagCollectionType } from './types'; -/** - * A single item in a parsed list, with optional nested sublist HTML. - */ -export interface ListItem { - text: string; - sub: string; -} -/** - * Result of parsing a list block: the generated HTML and the line - * index where the list ends (so the caller can advance past it). - */ -export interface ListResult { - html: string; - end: number; -} -export declare function childNodesToMarkdown(element: HTMLElement, callback: Function): string; -export declare class Tag implements TagType { - element: string[]; - opener: string[]; - closer: string; - pattern: RegExp | null; - precedence: number; - isBlock: boolean; - showDelimitersInEditorNodes: boolean; - tokenize(role: TokenRole, sequence: string, source: string): Token | null; - toHTML(token: Token, callback: Function): string; - toMarkdown(element: HTMLElement, callback: Function): string; - delimNodes(): [Node, Node]; - toEditorNode(token: Token, callback: Function): Node; -} -export declare class TagCollection implements TagCollectionType { - tags: Record; - ordered: TagType[]; - map: Map; - constructor(tags: TagType[]); - private validate; - getByElementName(element: string): TagType | undefined; -} -export declare class Bold extends Tag { - element: string[]; - aliases: string[]; - opener: string[]; - precedence: number; -} -export declare class Italic extends Tag { - element: string[]; - aliases: string[]; - opener: string[]; - precedence: number; -} -export declare class BoldItalic extends Tag { - element: string[]; - opener: string[]; - precedence: number; -} -export declare class Strikethrough extends Tag { - element: string[]; - aliases: string[]; - opener: string[]; -} -export declare class Code extends Tag { - element: string[]; - opener: string[]; -} -export declare class Anchor extends Tag { - opener: string[]; - element: string[]; - pattern: RegExp; - autolink_pattern: RegExp; - showDelimitersInEditorNodes: boolean; - tokenize(role: TokenRole, sequence: string, source: string): Token | null; - toHTML(token: Token, callback: Function): string; - toMarkdown(element: HTMLElement, callback: Function): string; -} -export declare class HardBreak extends Tag { - element: string[]; - template: string; - precedence: number; - pattern: RegExp; - isBlock: boolean; - toHTML(): string; - toMarkdown(): string; -} -/** - * Fenced code blocks: lines between ``` delimiters become
.
- *
- *   converter.toHTML('```js\nlet x = 1;\n```')
- *   // 
let x = 1;
- */ -export declare class FencedCode extends Tag { - opener: string[]; - closer: string; - pattern: RegExp; - isBlock: boolean; - element: string[]; - template: string; - replaceSelection: boolean; - toHTML(token: Token, callback: Function): string; - toMarkdown(element: HTMLElement, callback: Function): string; - toEditorNode(token: Token, callback: Function): Node; -} -/** - * Three or more *, -, or _ on a line become
. - * - * converter.toHTML('---') // '
' - */ -export declare class HorizontalRule extends Tag { - element: string[]; - template: string; - opener: string[]; - isBlock: boolean; - replaceSelection: boolean; - pattern: RegExp; - precedence: number; - toHTML(token: Token, callback: Function): string; - toMarkdown(element: HTMLElement, callback: Function): string; - toEditorNode(token: Token, callback: Function): Node; -} -export declare class Heading1 extends Tag { - element: string[]; - pattern: RegExp; - escapePattern: RegExp; - opener: string[]; - closer: string; - isBlock: boolean; - toHTML(token: Token, callback: Function): string; - toMarkdown(element: HTMLElement, callback: Function): string; - toEditorNode(token: Token, callback: Function): Node; - private anchorId; -} -export declare class Heading2 extends Heading1 { - element: string[]; - opener: string[]; -} -export declare class Heading3 extends Heading1 { - element: string[]; - opener: string[]; -} -export declare class Heading4 extends Heading1 { - element: string[]; - opener: string[]; -} -export declare class Heading5 extends Heading1 { - element: string[]; - opener: string[]; -} -export declare class Heading6 extends Heading1 { - element: string[]; - opener: string[]; -} -/** - * Lines prefixed with > become blockquotes. Consecutive > lines - * are merged into a single blockquote. - * - * converter.toHTML('> hello\n> world') - * //

hello\nworld

- */ -export declare class Blockquote extends Tag { - element: string[]; - opener: string[]; - closer: string; - isBlock: boolean; - template: string; - pattern: RegExp; - private childToEditorNode; - /** - * Split a paragraph's content into lines at
elements. - */ - private paragraphToLines; - toHTML(token: Token, callback: Function): string; - toMarkdown(element: HTMLElement, callback: Function): string; - toEditorNode(token: Token, callback: Function): Node; -} -/** - * Ordered and unordered lists with arbitrary nesting. Indentation - * determines depth; mixed list types (ul inside ol) are supported. - * - * converter.toHTML('- one\n- two\n 1. nested') - */ -declare class List extends Tag { - pattern: RegExp; - orderedOpenerPattern: RegExp; - isBlock: boolean; - toHTML(token: Token, callback: Function): string; - toMarkdown(element: HTMLElement, callback: Function): string; - toEditorNode(token: Token, callback: Function): Node; - private childToEditorNode; - private countListLines; - /** - * Recursively parse markdown list lines into HTML, handling nested - * sublists at arbitrary depth and mixed list types (ul/ol). - */ - private parseBlock; - /** - * Convert an HTML list back to markdown. Recurses into nested - * sublists with 2-space indentation per depth level. - */ - private nodeToMarkdown; - private partitionListItem; -} -export declare class UnorderedList extends List { - element: string[]; - opener: string[]; - template: string; -} -export declare class OrderedList extends List { - element: string[]; - opener: string[]; - template: string; -} -/** - * Pipe-delimited tables with optional column alignment. - * - * converter.toHTML('| A | B |\n|---|---|\n| 1 | 2 |') - */ -export declare class Table extends Tag { - opener: string[]; - element: string[]; - template: string; - replaceSelection: boolean; - pattern: RegExp; - tableSeparatorPattern: RegExp; - isBlock: boolean; - showDelimitersInEditorNodes: boolean; - parseBlock(token: Token): Record | null; - toHTML(token: Token, callback: Function): string; - toMarkdown(element: HTMLElement, callback: Function): string; - private parseRow; - private parseAligns; -} -export {}; diff --git a/dist/tags.js b/dist/tags.js deleted file mode 100644 index 9354855..0000000 --- a/dist/tags.js +++ /dev/null @@ -1,734 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Table = exports.OrderedList = exports.UnorderedList = exports.Blockquote = exports.Heading6 = exports.Heading5 = exports.Heading4 = exports.Heading3 = exports.Heading2 = exports.Heading1 = exports.HorizontalRule = exports.FencedCode = exports.HardBreak = exports.Anchor = exports.Code = exports.Strikethrough = exports.BoldItalic = exports.Italic = exports.Bold = exports.TagCollection = exports.Tag = void 0; -exports.childNodesToMarkdown = childNodesToMarkdown; -const hopdown_1 = require("./hopdown"); -/** - * Patterns shared across multiple tag classes. Defined once here - * so the regexes aren't duplicated and can be referenced by name. - */ -const blankLine = /^\s*$/; -function childNodesToMarkdown(element, callback) { - return Array.from(element.childNodes) - .map(child => callback(child)) - .join(''); -} -class Tag { - element = ['']; - opener = ['']; - closer = ''; - pattern = null; - precedence = 50; - isBlock = false; - showDelimitersInEditorNodes = true; - tokenize(role, sequence, source) { - if (this.pattern) { - const match = this.pattern.exec(source); - if (!match?.groups) { - return null; - } - return { - 'role': 'block', - 'sequence': sequence, - 'value': match.groups.captured, - 'groups': match.groups, - 'tag': this - }; - } - return { - role: role, - sequence: sequence, - value: sequence, - tag: this, - }; - } - toHTML(token, callback) { - if (token.role == 'open') { - return this.element.map(el => { return `<${el.toUpperCase()}>`; }).join(''); - } - if (token.role == 'close') { - const rev = this.element; - rev.reverse(); - return rev.map(el => { return ``; }).join(''); - } - if (token.role == 'block') { - const res = callback(token.value); - return res.firstElementChild?.innerHTML || res; - } - return token.value; - } - ; - toMarkdown(element, callback) { - const children = childNodesToMarkdown(element, callback); - return `${this.opener[0]}${children || element.textContent || ''}${this.closer || this.opener[0]}`; - } - ; - delimNodes() { - const opener = document.createElement('span'); - opener.classList.add('delim'); - for (const el of this.element) { - opener.classList.add(el.toLowerCase()); - } - ; - opener.textContent = this.opener[0]; - const closer = opener.cloneNode(true); - closer.textContent = this.closer || this.opener[0]; - return [opener, closer]; - } - toEditorNode(token, callback) { - console.log(token); - if (['open', 'close', 'block'].indexOf(token.role) == -1) { - return document.createTextNode(token.value); - } - const [opener, closer] = this.delimNodes(); - if (token.role == 'block') { - const div = document.createElement('div'); - div.innerHTML = this.toHTML(token, callback); - const el = div.firstChild; - if (token.tag.showDelimitersInEditorNodes) { - if (this.opener || this.closer) { - el.insertBefore(opener, el.firstChild); - el.appendChild(closer); - } - } - return el; - } - if (token.role == 'close') { - return closer; - } - return opener; - } - ; -} -exports.Tag = Tag; -class TagCollection { - tags = {}; - ordered; - map; - //openers: RegExp[]; - constructor(tags) { - for (const tag of tags) { - if (tag.constructor.name in this.tags) { - throw new Error(`Duplicate tag instances found for class ${tag.constructor.name}! Do you need to subclass it?`); - } - this.tags[tag.constructor.name] = tag; - } - this.ordered = tags.sort((a, b) => { - const p1 = a.precedence - (a.isBlock ? 100 : 0); - const p2 = b.precedence - (b.isBlock ? 100 : 0); - return p1 < p2 ? -1 : 1; - }); - //this.validate(); - this.map = new Map(); - for (const tag of this.ordered) { - for (const key of tag.opener) { - if (!this.map.has(key)) { - this.map.set(key, []); - } - this.map.get(key).push(tag); - } - } - } - validate() { - const inline = this.ordered.filter(t => { return !t.opener; }); - for (let outer = 0; outer < inline.length; outer++) { - for (let inner = outer + 1; inner < inline.length; inner++) { - const first = this.ordered[outer]; - const second = this.ordered[inner]; - if (!(first.opener && second.opener)) { - continue; - } - const firstIsPrefix = second.opener[0].startsWith(first.opener[0]); - const secondIsPrefix = first.opener[0].startsWith(second.opener[0]); - if (!firstIsPrefix && !secondIsPrefix) { - continue; - } - const longer = first.opener.length > second.opener.length ? first : second; - const shorter = first.opener.length > second.opener.length ? second : first; - if (longer.precedence >= shorter.precedence) { - throw new Error(`Tag "${longer.constructor.name}" (opener "${longer.opener}") must have ` + - `lower precedence than "${shorter.constructor.name}" (opener "${shorter.opener}") ` + - `because its opener is a prefix match. ` + - `Got ${longer.constructor.name}=${longer.precedence}, ${shorter.constructor.name}=${shorter.precedence}.`); - } - } - } - } - getByElementName(element) { - const el = element.toUpperCase(); - return this.ordered.filter(t => { - return [...t.element, ...t.aliases ?? []].includes(el); - })[0] || undefined; - } -} -exports.TagCollection = TagCollection; -class Bold extends Tag { - element = ['STRONG']; - aliases = ['B']; - opener = ['**', '__']; - precedence = 40; -} -exports.Bold = Bold; -class Italic extends Tag { - element = ['EM']; - aliases = ['I']; - opener = ['*', '_']; - precedence = 30; -} -exports.Italic = Italic; -class BoldItalic extends Tag { - element = ['STRONG', 'EM']; - opener = ['***']; - precedence = 50; -} -exports.BoldItalic = BoldItalic; -class Strikethrough extends Tag { - element = ['DEL']; - aliases = ['S', 'STRIKE']; - opener = ['~~']; -} -exports.Strikethrough = Strikethrough; -class Code extends Tag { - element = ['CODE']; - opener = ['`']; -} -exports.Code = Code; -class Anchor extends Tag { - opener = ['[']; - element = ['A']; - pattern = /(?\[(?[^\[\]]+)\]\((?[^\s)]+)(?:\s+"(?[^"]*)")?\))/; - autolink_pattern = /^(?<captured>(?<url>[A-Za-z]+:\/\/[^\s>]+))/; - showDelimitersInEditorNodes = false; - tokenize(role, sequence, source) { - const match = (this.pattern.exec(source) || - this.autolink_pattern.exec(source)); - if (match?.groups) { - const token = { - 'role': 'block', - 'sequence': sequence, - 'value': match.groups.captured, - 'groups': match.groups, - 'tag': this - }; - return token; - } - return null; - } - toHTML(token, callback) { - const title = (0, hopdown_1.escapeHtml)(token.groups.title || ''); - const url = (0, hopdown_1.escapeHtml)(token.groups.url || ''); - const text = (0, hopdown_1.escapeHtml)(token.groups.text || '') || url; - return `<A HREF="${url}" TITLE="${title}">${text}</A>`; - } - toMarkdown(element, callback) { - const href = element.getAttribute('href') || ''; - const title = element.getAttribute('title'); - const titlePart = title ? ` "${title}"` : ''; - return '[' + element.innerText + '](' + href + titlePart + ')'; - } -} -exports.Anchor = Anchor; -class HardBreak extends Tag { - element = ['BR']; - template = ' \n'; - precedence = 20; - pattern = blankLine; - isBlock = true; - toHTML() { return '<BR>'; } - toMarkdown() { return ' \n'; } -} -exports.HardBreak = HardBreak; -/** - * Fenced code blocks: lines between ``` delimiters become <pre><code>. - * - * converter.toHTML('```js\nlet x = 1;\n```') - * // <pre><code class="language-js">let x = 1;</code></pre> - */ -class FencedCode extends Tag { - opener = ['```']; - closer = ''; - pattern = /(?<captured>^(?<fence>```)(?<lang>\w*)\s*\n(?<code>(.+\n)+)\k<fence>)/m; - isBlock = true; - element = ['PRE']; - template = '```\n10 PRINT "HELLO"\n20 GOTO 10\n```'; - replaceSelection = true; - toHTML(token, callback) { - const fence = token.groups.fence; - const lang = token.groups.lang.trim(); - var code = token.groups.code.replace(/^\n|\n$/g, ''); - code = `\n${code}\n`; - const langAttr = lang - ? ` class="language-${(0, hopdown_1.escapeHtml)(lang)}"` - : ''; - const val = token.value.slice(token.groups.fence.length + token.groups.lang.length, token.value.length - token.groups.fence.length); - return `<pre${langAttr}>${(0, hopdown_1.escapeHtml)(val)}</pre>`; - } - toMarkdown(element, callback) { - const lang = element.className.match(/language-(\S+)/)?.[1] || ''; - const content = element?.textContent || ''; - return '\n\n' + this.opener[0] + lang + content + this.opener[0] + '\n\n'; - } - toEditorNode(token, callback) { - const div = document.createElement('div'); - div.innerHTML = this.toHTML(token, callback); - const [opener, closer] = this.delimNodes(); - const el = div.firstChild; - if (el) { - el.insertBefore(opener, el.firstChild); - el.appendChild(closer); - } - return el; - } -} -exports.FencedCode = FencedCode; -/** - * Three or more *, -, or _ on a line become <hr>. - * - * converter.toHTML('---') // '<hr>' - */ -class HorizontalRule extends Tag { - element = ['HR']; - template = '***'; - opener = ['***', '___', '---']; - isBlock = true; - replaceSelection = false; - pattern = /^(?<captured>(\*{3,}|-{3,}|_{3,}))\s*$/; - precedence = 20; - toHTML(token, callback) { - return `<${this.element[0].toUpperCase()}>`; - } - toMarkdown(element, callback) { - return '\n' + this.opener[0] + '\n'; - } - toEditorNode(token, callback) { - return document.createElement('hr'); - } -} -exports.HorizontalRule = HorizontalRule; -class Heading1 extends Tag { - element = ['H1']; - pattern = /^(?<captured>(?<hashes>\#{1,6})\s+(?<content>.*?$))/m; - escapePattern = /[.*+?^${}()|[\]\\]/g; - opener = ['#']; - closer = ''; - isBlock = true; - toHTML(token, callback) { - const level = token.groups.hashes.length; - const id = this.anchorId(token.groups.content); - return `<h${level} id='${id}'>${callback(token.groups.content)}</h${level}>`; - } - toMarkdown(element, callback) { - const inner = childNodesToMarkdown(element, callback); - const level = parseInt(element.nodeName[1]); - return '\n\n' + '#'.repeat(level) + ' ' + inner + '\n\n'; - } - toEditorNode(token, callback) { - const content = token.groups.content; - const id = this.anchorId(content); - const heading = document.createElement(this.element[0]); - heading.innerHTML = content; - heading.setAttribute('id', id); - return heading; - } - anchorId(text) { - return text.replace(this.escapePattern, ' ').trim().split(/\s+/).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join('_'); - } -} -exports.Heading1 = Heading1; -class Heading2 extends Heading1 { - element = ['H2']; - opener = ['##']; -} -exports.Heading2 = Heading2; -; -class Heading3 extends Heading1 { - element = ['H3']; - opener = ['###']; -} -exports.Heading3 = Heading3; -; -class Heading4 extends Heading1 { - element = ['H4']; - opener = ['####']; -} -exports.Heading4 = Heading4; -; -class Heading5 extends Heading1 { - element = ['H5']; - opener = ['#####']; -} -exports.Heading5 = Heading5; -; -class Heading6 extends Heading1 { - element = ['H6']; - opener = ['######']; -} -exports.Heading6 = Heading6; -; -/** - * Lines prefixed with > become blockquotes. Consecutive > lines - * are merged into a single blockquote. - * - * converter.toHTML('> hello\n> world') - * // <blockquote><p>hello\nworld</p></blockquote> - */ -class Blockquote extends Tag { - element = ['BLOCKQUOTE']; - opener = ['> ']; - closer = ''; - isBlock = true; - template = '> Quote\n> continues here'; - pattern = /(?<captured>(?:\>\s+.*?(\n|$))+)$/m; - childToEditorNode(node, callback) { - var nodes = []; - for (const child of node.childNodes) { - if (child.nodeType == 11) { - nodes.concat(this.childToEditorNode(child, callback)); - } - else if (child.textContent) { - nodes.push(child); - } - } - return nodes; - } - ; - /** - * Split a paragraph's content into lines at <br> elements. - */ - paragraphToLines(paragraph, callback) { - const lines = []; - let currentLine = ''; - for (const child of Array.from(paragraph.childNodes)) { - if (child.nodeType === 1 && child.tagName === 'BR') { - lines.push(currentLine); - currentLine = ''; - } - else { - currentLine += callback(child); - } - } - if (currentLine.trim()) { - lines.push(currentLine); - } - return lines; - } - toHTML(token, callback) { - var html = `<${this.element[0].toLowerCase()}>`; - var inner = []; - var pFlag = false; - for (var line of token.value.split('\n')) { - var text = line.replace(/^>\s*/, ''); - if (text) { - html += callback(text) + "<br>"; - continue; - } - html += pFlag ? "</p>" : "<p>"; - pFlag = !pFlag; - } - html = html.replace(/\<br\>$/g, pFlag ? '</p>' : ''); - html += `</${this.element[0].toLowerCase()}>`; - return html; - } - toMarkdown(element, callback) { - // Each <p> inside the blockquote is a paragraph group. - // Within a <p>, <br> elements create line breaks. - // Separate paragraphs get a blank > line between them. - const paragraphs = []; - for (const child of Array.from(element.childNodes)) { - const el = child; - if (child.nodeType === 1 && el.tagName === 'P') { - const lines = this.paragraphToLines(el, callback); - paragraphs.push(this.opener[0]); - paragraphs.push(lines.map(line => '> ' + line).join('\n')); - } - else { - const text = callback(child).trim(); - if (text) { - paragraphs.push('> ' + text); - } - } - } - return '\n\n' + paragraphs.join('\n') + '\n\n'; - } - toEditorNode(token, callback) { - const [opener, closer] = this.delimNodes(); - opener.textContent = opener.textContent.replaceAll(' ', ''); - const bq = document.createElement('blockquote'); - for (const line of (token.value).split('\n')) { - if (line === '') { - continue; - } - const inner = callback(line.replace(/^>\s?/, '')); - const children = this.childToEditorNode(inner, callback); - const p = document.createElement('P'); - for (const c of children) { - p.innerHTML += c.innerHTML; - } - p.insertBefore(opener.cloneNode(true), p.firstChild); - bq.appendChild(p); - } - return bq; - } -} -exports.Blockquote = Blockquote; -/** - * Ordered and unordered lists with arbitrary nesting. Indentation - * determines depth; mixed list types (ul inside ol) are supported. - * - * converter.toHTML('- one\n- two\n 1. nested') - */ -class List extends Tag { - pattern = /(?<captured>(?<lines>^(?:\s{2,})?(?:[-+*]|\d+\.)\s.+?(?:\n|$))+)/m; - orderedOpenerPattern = /^\d+\.\s/; - isBlock = true; - toHTML(token, callback) { - const result = this.parseBlock(token.groups.captured.split("\n"), 0, 0, callback); - return result.html; - } - toMarkdown(element, callback) { - return this.nodeToMarkdown(element, 0, callback); - } - toEditorNode(token, callback) { - const matched = token.value.match(this.pattern); - if (matched === null) { - throw new Error(`Token value does not match pattern ${this.pattern}: '${token.value}'`); - } - const parsed = this.parseBlock(token.value.split('\n'), 0, 0, callback); - const div = document.createElement('div'); - div.innerHTML = parsed.html; - return div.firstChild; - } - childToEditorNode(node, callback) { - var nodes = []; - for (const child of node.childNodes) { - if (child.nodeType == 11) { - nodes.concat(this.childToEditorNode(child, callback)); - } - else if (child.textContent) { - nodes.push(child); - } - } - return nodes; - } - ; - countListLines(lines, start) { - const indentedUnordered = /^(\s*)[*\-+]\s/; - const indentedOrdered = /^(\s*)\d+\.\s/; - let lineIndex = start; - while (lineIndex < lines.length) { - const line = lines[lineIndex]; - if (blankLine.test(line)) { - break; - } - if (!indentedUnordered.test(line) && !indentedOrdered.test(line)) { - break; - } - lineIndex++; - } - return lineIndex - start; - } - /** - * Recursively parse markdown list lines into HTML, handling nested - * sublists at arbitrary depth and mixed list types (ul/ol). - */ - parseBlock(lines, start = 0, indent = 0, callback) { - const prefix = new RegExp('^' - + ' '.repeat(indent) // indentation for this nesting level - + '([\\*\\-+]|\\d+\\.)' // bullet or number marker - + '\\s' // space after marker - ); - const isOl = this.orderedOpenerPattern.test(lines[start].trim()); - const tag = isOl ? 'ol' : 'ul'; - const items = []; - let lineIndex = start; - while (lineIndex < lines.length) { - const line = lines[lineIndex]; - if (blankLine.test(line)) { - break; - } - const leadingWhitespace = /^(\s*)/; - const lineIndent = line.match(leadingWhitespace)[1].length; - if (lineIndent < indent) { - break; - } - if (lineIndent > indent) { - const sub = this.parseBlock(lines, lineIndex, lineIndent, callback); - items[items.length - 1].sub = sub.html; - lineIndex = sub.end; - continue; - } - if (!prefix.test(line)) { - break; - } - items.push({ - text: line.replace(prefix, ''), - sub: '', - }); - lineIndex++; - } - var html = `<${tag}>`; - for (var item of items) { - const res = callback(item.text); - html += '<li>' + (res.firstElementChild?.innerHTML || res) + item.sub + '</li>'; - } - html += `</${tag}>`; - return { - html, - end: lineIndex, - }; - } - /** - * Convert an HTML list back to markdown. Recurses into nested - * sublists with 2-space indentation per depth level. - */ - nodeToMarkdown(node, depth, callback) { - const isOl = node.nodeName === 'OL'; - const indent = ' '.repeat(depth); - const lines = []; - Array.from(node.children).forEach((listItem, index) => { - const marker = isOl ? (index + 1) + '. ' : '* '; - const { text, sublist } = this.partitionListItem(listItem, depth, callback); - lines.push(indent + marker + text.trim()); - if (sublist) { - lines.push(sublist); - } - }); - const result = lines.join('\n'); - return depth === 0 - ? '\n\n' + result + '\n\n' - : result; - } - partitionListItem(listItem, depth, callback) { - let text = ''; - let sublist = ''; - for (const child of Array.from(listItem.childNodes)) { - if (child.nodeType === 1 && (child.nodeName === 'UL' || child.nodeName === 'OL')) { - sublist += this.nodeToMarkdown(child, depth + 1, callback); - } - else { - text += callback(child); - } - } - return { - text, - sublist, - }; - } -} -class UnorderedList extends List { - element = ['UL']; - opener = ['*', '-']; - template = '* list\n* items \n* here'; -} -exports.UnorderedList = UnorderedList; -class OrderedList extends List { - element = ['OL']; - opener = ['1.']; - template = '1. list\n1. items \n1. here'; -} -exports.OrderedList = OrderedList; -/** - * Pipe-delimited tables with optional column alignment. - * - * converter.toHTML('| A | B |\n|---|---|\n| 1 | 2 |') - */ -class Table extends Tag { - opener = ['|']; - element = ['TABLE']; - template = '| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |'; - replaceSelection = false; - pattern = /(?<captured>(?:\|.+?\|\s*(?:\n|$))+)/m; - tableSeparatorPattern = new RegExp('^' - + '\\|?' // optional leading pipe - + '\\s*:?-+:?\\s*' // first column separator (optional alignment colons) - + '(' - + '\\|' // pipe between columns - + '\\s*:?-+:?\\s*' // subsequent column separator - + ')*' // zero or more additional columns - + '\\|?' // optional trailing pipe - + '\\s*$' // end of line - ); - isBlock = true; - showDelimitersInEditorNodes = false; - parseBlock(token) { - const lines = token.groups.captured.split("\n"); - // Second line must be the separator row (e.g. |---|---|) - if (!this.tableSeparatorPattern.test(lines[1])) { - return null; - } - const headers = this.parseRow(lines[0]); - const aligns = this.parseAligns(lines[1]); - const rows = []; - let lineIndex = 2; - while (lineIndex < lines.length && lines[lineIndex].indexOf('|') !== -1 && !blankLine.test(lines[lineIndex])) { - rows.push(this.parseRow(lines[lineIndex++])); - } - return { - headers: headers, - aligns: aligns, - rows: rows - }; - } - toHTML(token, callback) { - const block = this.parseBlock(token); - if (!block) { - throw new Error(`Could not parse table token! ${token.value}`); - } - const cell = (tag, text, index) => { - const align = block.aligns[index] - ? ` class="align-${block.aligns[index]}"` - : ''; - const res = callback(text); - return `<${tag}${align}>` + (res.firstElementChild?.innerHTML || res) + `</${tag}>`; - }; - const head = '<thead><tr>' - + block.headers.map((text, column) => cell('th', text, column)).join('') - + '</tr></thead>'; - const body = block.rows.map((row) => '<tr>' + row.map((text, column) => cell('td', text, column)).join('') + '</tr>').join(''); - return `<table>${head}<tbody>${body}</tbody></table>`; - } - toMarkdown(element, callback) { - const rows = Array.from(element.querySelectorAll('tr')); - if (!rows.length) { - return ''; - } - const headers = Array.from(rows[0].querySelectorAll('th,td')) - .map(cell => callback(cell).trim()); - const separator = Array.from(element.querySelectorAll('th')).map(h => { - return h.className == 'align-center' ? ':---:' : - h.className == 'align-right' ? '---:' : - '---'; - }); - const output = [ - '| ' + headers.join(' | ') + ' |', - '| ' + separator.join(' | ') + ' |', - ]; - for (const row of rows.slice(1)) { - const cells = Array.from(row.querySelectorAll('td,th')) - .map(cell => callback(cell).trim()); - output.push('| ' + cells.join(' | ') + ' |'); - } - return '\n\n' + output.join('\n') + '\n\n'; - } - parseRow(line) { - return line - .replace(/^\|/, '') - .replace(/\|$/, '') - .split('|') - .map(cell => cell.trim()); - } - parseAligns(line) { - const centerAlign = /^:-+:$/; - const rightAlign = /^-+:$/; - return this.parseRow(line).map(cell => { - if (centerAlign.test(cell)) { - return 'center'; - } - if (rightAlign.test(cell)) { - return 'right'; - } - return null; - }); - } -} -exports.Table = Table; diff --git a/dist/tags/base.d.ts b/dist/tags/base.d.ts index 53144e9..d7d9b5a 100644 --- a/dist/tags/base.d.ts +++ b/dist/tags/base.d.ts @@ -3,6 +3,7 @@ export declare class Tag implements TagType { element: string[]; opener: string[]; closer: string; + aliases: string[]; pattern: RegExp | null; precedence: number; isBlock: boolean; diff --git a/dist/tags/base.js b/dist/tags/base.js index e673f0d..4864ce7 100644 --- a/dist/tags/base.js +++ b/dist/tags/base.js @@ -5,6 +5,7 @@ class Tag { element = ['']; opener = ['']; closer = ''; + aliases = []; pattern = null; precedence = 50; isBlock = false; @@ -70,7 +71,6 @@ class Tag { return [opener, closer]; } toEditorNode(token, callback) { - console.log(token); if (['open', 'close', 'block'].indexOf(token.role) == -1) { return document.createTextNode(token.value); } @@ -124,7 +124,7 @@ class TagCollection { getByElementName(element) { const el = element.toUpperCase(); return this.ordered.filter(t => { - return t.element.includes(el); + return [...t.element, ...t.aliases ?? []].includes(el); })[0] || undefined; } } diff --git a/dist/tags/index.d.ts b/dist/tags/index.d.ts index 2ceccd4..18fcb45 100644 --- a/dist/tags/index.d.ts +++ b/dist/tags/index.d.ts @@ -1,10 +1,11 @@ export * from "./base"; export * from "./anchor"; -export * from "./blockquote"; export * from "./fenced"; export * from "./hardbreak"; export * from "./heading"; export * from "./hr"; +export * from "./image"; +export * from "./blockquote"; export * from "./inline"; export * from "./list"; export * from "./table"; diff --git a/dist/tags/index.js b/dist/tags/index.js index 96d7a52..ba08955 100644 --- a/dist/tags/index.js +++ b/dist/tags/index.js @@ -16,11 +16,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) { Object.defineProperty(exports, "__esModule", { value: true }); __exportStar(require("./base"), exports); __exportStar(require("./anchor"), exports); -__exportStar(require("./blockquote"), exports); __exportStar(require("./fenced"), exports); __exportStar(require("./hardbreak"), exports); __exportStar(require("./heading"), exports); __exportStar(require("./hr"), exports); +__exportStar(require("./image"), exports); +__exportStar(require("./blockquote"), exports); __exportStar(require("./inline"), exports); __exportStar(require("./list"), exports); __exportStar(require("./table"), exports); diff --git a/dist/tags/inline.d.ts b/dist/tags/inline.d.ts index 9b35321..bb87c04 100644 --- a/dist/tags/inline.d.ts +++ b/dist/tags/inline.d.ts @@ -1,11 +1,13 @@ import { Tag } from "./base"; export declare class Bold extends Tag { element: string[]; + aliases: string[]; opener: string[]; precedence: number; } export declare class Italic extends Tag { element: string[]; + aliases: string[]; opener: string[]; precedence: number; } @@ -16,6 +18,7 @@ export declare class BoldItalic extends Tag { } export declare class Strikethrough extends Tag { element: string[]; + aliases: string[]; opener: string[]; } export declare class Code extends Tag { diff --git a/dist/tags/inline.js b/dist/tags/inline.js index 9d094ce..8d04c94 100644 --- a/dist/tags/inline.js +++ b/dist/tags/inline.js @@ -3,13 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.Code = exports.Strikethrough = exports.BoldItalic = exports.Italic = exports.Bold = void 0; const base_1 = require("./base"); class Bold extends base_1.Tag { - element = ['STRONG', 'B']; + element = ['STRONG']; + aliases = ['B']; opener = ['**', '__']; precedence = 40; } exports.Bold = Bold; class Italic extends base_1.Tag { - element = ['EM', 'I']; + element = ['EM']; + aliases = ['I']; opener = ['*', '_']; precedence = 30; } @@ -21,7 +23,8 @@ class BoldItalic extends base_1.Tag { } exports.BoldItalic = BoldItalic; class Strikethrough extends base_1.Tag { - element = ['DEL', 'S', 'STRIKE']; + element = ['DEL']; + aliases = ['S', 'STRIKE']; opener = ['~~']; } exports.Strikethrough = Strikethrough; diff --git a/dist/types.d.ts b/dist/types.d.ts index 84a99e2..18b5648 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -11,6 +11,7 @@ export interface Token { } export interface TagType { element: string[]; + aliases: string[]; opener: string[]; closer: string | null; precedence: number; diff --git a/src/ts/defaults.ts b/src/ts/defaults.ts index 09eb32f..c16bb4a 100644 --- a/src/ts/defaults.ts +++ b/src/ts/defaults.ts @@ -8,6 +8,7 @@ export const defaultTags = new tags.TagCollection([ new tags.Strikethrough(), new tags.Code(), new tags.Anchor(), + new tags.Image(), new tags.HardBreak(), new tags.FencedCode(), new tags.HorizontalRule(), diff --git a/src/ts/tags/base.ts b/src/ts/tags/base.ts index ff03842..819b723 100644 --- a/src/ts/tags/base.ts +++ b/src/ts/tags/base.ts @@ -4,6 +4,7 @@ export class Tag implements TagType { element: string[] = ['']; opener: string[] = ['']; closer: string = ''; + aliases: string[] = []; pattern: RegExp | null = null; precedence: number = 50; isBlock: boolean = false; @@ -74,7 +75,6 @@ export class Tag implements TagType { } toEditorNode(token: Token, callback: Function): Node { - console.log(token); if (['open', 'close', 'block'].indexOf(token.role) == -1) { return document.createTextNode(token.value); } @@ -132,7 +132,7 @@ export class TagCollection implements TagCollectionType { getByElementName(element: string): TagType | undefined { const el = element.toUpperCase(); return this.ordered.filter(t => { - return t.element.includes(el); + return [...t.element, ...t.aliases ?? []].includes(el); })[0] || undefined; } } diff --git a/src/ts/tags/index.ts b/src/ts/tags/index.ts index 2ceccd4..18fcb45 100644 --- a/src/ts/tags/index.ts +++ b/src/ts/tags/index.ts @@ -1,10 +1,11 @@ export * from "./base"; export * from "./anchor"; -export * from "./blockquote"; export * from "./fenced"; export * from "./hardbreak"; export * from "./heading"; export * from "./hr"; +export * from "./image"; +export * from "./blockquote"; export * from "./inline"; export * from "./list"; export * from "./table"; diff --git a/src/ts/tags/inline.ts b/src/ts/tags/inline.ts index ce56ff5..f02cf36 100644 --- a/src/ts/tags/inline.ts +++ b/src/ts/tags/inline.ts @@ -1,13 +1,15 @@ import { Tag } from "./base"; export class Bold extends Tag { - element = ['STRONG', 'B']; + element = ['STRONG']; + aliases = ['B']; opener = ['**', '__']; precedence = 40; } export class Italic extends Tag { - element = ['EM', 'I']; + element = ['EM']; + aliases = ['I']; opener = ['*', '_']; precedence = 30; } @@ -19,7 +21,8 @@ export class BoldItalic extends Tag { } export class Strikethrough extends Tag { - element = ['DEL', 'S', 'STRIKE']; + element = ['DEL']; + aliases = ['S', 'STRIKE']; opener = ['~~']; } diff --git a/src/ts/types.ts b/src/ts/types.ts index c769f87..d8b7e0d 100644 --- a/src/ts/types.ts +++ b/src/ts/types.ts @@ -11,6 +11,7 @@ export interface Token { export interface TagType { element: string[]; + aliases: string[]; opener: string[]; closer: string | null; precedence: number; diff --git a/test/hopdown.test.ts b/test/hopdown.test.ts index 0e4c5ad..a690eb7 100644 --- a/test/hopdown.test.ts +++ b/test/hopdown.test.ts @@ -24,6 +24,7 @@ suite('Markdown to HTML Round-Trips', () => { assert('inline code', '`code`', '<p><code>code</code></p>'); assert('bold+italic', '***bi***', '<p><strong><em>bi</em></strong></p>'); assert('link', '[t](http://x)', '<p><a href="http://x" title="">t</a></p>'); + assert('image', '![text](src.png "title")', '<p><img src="src.png" alt="text" title="title"></p>'); assert('mixed', 'a **b** *c* `d`', '<p>a <strong>b</strong> <em>c</em> <code>d</code></p>'); assert('code then bold', '`a` **b**', '<p><code>a</code> <strong>b</strong></p>'); assert('nested, ummatched italic', '**i* b**', '<p><strong>i* b</strong></p>'); @@ -293,4 +294,9 @@ suite('Markdown to Editor Nodes Round-Trips', () => { assertInline('link', '[label](url "title")', '<a href="url" title="title">label</a>'); }); + describe('image', () => { + assertInline('image', '![alt](url)', '<img src="url" alt="alt" title="">'); + assertInline('image', '![alt](url "title")', '<img src="url" alt="alt" title="title">'); + }); + });