implement images
This commit is contained in:
1 parent
9a2ba8c330
commit
a306820a78
16 files changed
+36
-979
No files matched your search
Vendored
+1
@@ -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(),
|
||||
|
||||
Vendored
-232
@@ -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<string, TagType>;
|
||||
ordered: TagType[];
|
||||
map: Map<string, TagType[]>;
|
||||
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 <pre><code>.
|
||||
*
|
||||
* converter.toHTML('```js\nlet x = 1;\n```')
|
||||
* // <pre><code class="language-js">let x = 1;</code></pre>
|
||||
*/
|
||||
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 <hr>.
|
||||
*
|
||||
* converter.toHTML('---') // '<hr>'
|
||||
*/
|
||||
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')
|
||||
* // <blockquote><p>hello\nworld</p></blockquote>
|
||||
*/
|
||||
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 <br> 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<string, any> | null;
|
||||
toHTML(token: Token, callback: Function): string;
|
||||
toMarkdown(element: HTMLElement, callback: Function): string;
|
||||
private parseRow;
|
||||
private parseAligns;
|
||||
}
|
||||
export {};
|
||||
Vendored
-734
@@ -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 `</${el.toUpperCase()}>`; }).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 = /(?<captured>\[(?<text>[^\[\]]+)\]\((?<url>[^\s)]+)(?:\s+"(?<title>[^"]*)")?\))/;
|
||||
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;
|
||||
Vendored
+1
@@ -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;
|
||||
|
||||
Vendored
+2
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
-1
@@ -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";
|
||||
Vendored
+2
-1
@@ -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);
|
||||
Vendored
+3
@@ -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 {
|
||||
|
||||
Vendored
+6
-3
@@ -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;
|
||||
|
||||
Vendored
+1
@@ -11,6 +11,7 @@ export interface Token {
|
||||
}
|
||||
export interface TagType {
|
||||
element: string[];
|
||||
aliases: string[];
|
||||
opener: string[];
|
||||
closer: string | null;
|
||||
precedence: number;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -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 = ['~~'];
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface Token {
|
||||
|
||||
export interface TagType {
|
||||
element: string[];
|
||||
aliases: string[];
|
||||
opener: string[];
|
||||
closer: string | null;
|
||||
precedence: number;
|
||||
|
||||
@@ -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', '', '<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', '', '<img src="url" alt="alt" title="">');
|
||||
assertInline('image', '', '<img src="url" alt="alt" title="title">');
|
||||
});
|
||||
|
||||
});
|
||||
Reference in new issue
Block a user