UNPKG

markdown-it-wikirefs

Version:

markdown-it plugin to add wikirefs (including [[wikilinks]])

1,147 lines (1,064 loc) 42 kB
// markdown-it-wikirefs v0.0.8 - https://github.com/wikibonsai/markdown-it-wikirefs.git import { defu } from 'defu'; import * as wikirefs from 'wikirefs'; // perform any file preparations before we do anything -- // (like flush the current file's entry in index if there is one) const prep_file = (md, opts) => { // insert the 'prep_file' token after all wikirefs have been tokenized. // this way, when 'prep_file' calls 'unshift' (see above) it will place the // render rule in front of all other tokens. otherwise, it might get shuffled // into an index other than the first one. md.core.ruler.push('prep_file', prep_file); function prep_file(state) { if (opts.prepFile) { const tok = new state.Token('trigger_prep_file', '', 0); state.tokens.unshift(tok); } } md.renderer.rules.trigger_prep_file = trigger_prep_file; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function trigger_prep_file(tokens, index, mdOpts, env) { if (opts.prepFile) { opts.prepFile(env); } return ''; } }; const wikiattrs = (md, opts) => { // rulers // '.getRules('attrs')' is really testing for markdown-it-'caml' // 'attrs' is added as an extra dummy 'alt' specifically for this purpose // (directly accessing the 'caml' rule would be ideal, but markdown-it // doesn't seem to have a mechanism for that and not sure how to // attach a 'name' property to sinon fakes) const attrRules = md.block.ruler.getRules('attrs'); if (opts.attrs.render && attrRules.length === 0) { // the 'attrbox' rule is the midpoint between the parse and render rules. md.core.ruler.after('inline', 'wiki_attrbox', wiki_attrbox); } // should execute just after 'markdown-it-caml': // [ ..., 'hr', 'caml', 'wikiattr', 'list', ... ] md.block.ruler.before('list', 'wikiattr', wikiattr, { alt: ['paragraph', 'reference', 'blockquote', 'list', 'attrs'] }); // in case bugs show up: [ 'paragraph', 'reference', 'blockquote', 'list' ] // render md.renderer.rules.metadata_wikiattr = metadata_wikiattr; md.renderer.rules.wikiattr_open = wikiattr_open; md.renderer.rules.wikiattr_key = wikiattr_key; md.renderer.rules.wikiattr_val = wikiattr_val; md.renderer.rules.wikiattr_close = wikiattr_close; // rulers function wikiattr(state, startLine, endLine, silent) { // return false cases // //// // skip indented code blocks // from: https://github.com/markdown-it/markdown-it/blob/df4607f1d4d4be7fdc32e71c04109aea8cc373fa/lib/rules_block/list.js#L132 // if it's indented more than 3 spaces, it should be a code block if (state.sCount[startLine] - state.blkIndent >= 4) { return false; } //// // 'wikiattrs' must be at the top-most-level // todo: how to interrupt? // !('list' | 'blockquote' | 'reference' | 'footnote') if (state.parentType !== 'root' && state.parentType !== 'paragraph') { return false; } //// // check for matches // 'bMarks' = beginning of line markers // 'eMarks' = end of line markers let pos = state.bMarks[startLine]; let max = state.eMarks[startLine]; const thisChunk = state.src.substring(pos, max); const lineOneMatch = wikirefs.RGX.ATTR_LINE.TYPE.exec(thisChunk); // no match if (lineOneMatch === null) { return false; } // is in a list item // note: this is only necessary for unprefixed wikiattrs // todo: keep an eye on this...might cause problems... if (lineOneMatch[0].indexOf('- ') === 0 || lineOneMatch[0].indexOf('* ') === 0 || lineOneMatch[0].indexOf('+ ') === 0) { return false; } // "Don't run any pairs in validation mode": // 'silent' is used when this rule is being checked against // in another rule to see whether or not the other rule should // kick out for this (or some other) one. return 'true' to // signify that the kick out should happen if (silent) { return true; } // handle match and return true // let iterLine = 0; let m; const curFilenames = []; const attrTypeText = lineOneMatch[1].trim(); const filenamesText = lineOneMatch[2]; // links // - comma-separated list; '2' would be the first wikilink's filename if (filenamesText !== null && filenamesText !== undefined) { iterLine += 1; const gottaCatchEmAll = new RegExp(`${wikirefs.RGX.WIKI.BASE.source}`, 'ig'); // loop through all matches from 'g'lobal regex // do-while: https://stackoverflow.com/a/6323598 do { m = gottaCatchEmAll.exec(lineOneMatch[0]); if (m !== null) { // m[0]: full match; // m[1]: filename; curFilenames.push(m[1]); } } while (m); // - markdown-style list } else { // loop through each markdown-style list item // do-while: https://stackoverflow.com/a/6323598 do { // increment iterLine += 1; pos = state.bMarks[startLine + iterLine]; max = state.eMarks[startLine + iterLine]; const thisSubChunk = state.src.substring(pos, max); m = wikirefs.RGX.ATTR_LINE.LIST_ITEM.exec(thisSubChunk); if (m !== null) { // m[0]: full match; // m[1]: bullet type; // m[2]: wikistring; // m[3]: filename; curFilenames.push(m[3]); } } while (m); } // set 'state.env.attrs' to trigger tokens -- if valid. if (curFilenames.length === 0) { return false; } else { // init if (!state.env.attrs) { state.env.attrs = {}; } if (!state.env.attrs[attrTypeText]) { state.env.attrs[attrTypeText] = []; } // prep renderables for (const fname of curFilenames) { state.env.attrs[attrTypeText].push({ type: 'wiki', filename: fname }); } // metadata if (opts.addAttr) { const tok = new state.Token('metadata_wikiattr', '', 0); state.tokens.push(tok); tok.attrSet('key', attrTypeText); // note: tokens technically should only accept 'string' or 'null'...but an array of strings works so nicely here... tok.attrSet('vals', state.env.attrs[attrTypeText].map(item => item.filename)); } // continue; increment position state.line += iterLine; return true; } } function wiki_attrbox(state) { if (!state.env.attrs || Object.keys(state.env.attrs).length === 0) { return; } const tokens = []; // open // const tokOpen = new state.Token('wikiattr_open', '', 0); // tokOpen.map = [startLine, iterLine]; tokens.push(tokOpen); // body // for (const attrtype in state.env.attrs) { // key / attrtype const tokType = new state.Token('wikiattr_key', '', 0); tokType.attrSet('key', attrtype); tokens.push(tokType); // values / items for (const item of state.env.attrs[attrtype]) { const tokItem = new state.Token('wikiattr_val', '', 1); if (item.type === 'wiki') { const filename = item.filename; if (!filename) { continue; } tokItem.attrSet('key', attrtype); tokItem.attrSet('val', filename); } tokens.push(tokItem); } } // close // const tokClose = new state.Token('wikiattr_close', '', 0); tokens.push(tokClose); // add infobox to **front** of token stream if (tokens) { state.tokens = tokens.concat(state.tokens); } } // tokens /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function metadata_wikiattr(tokens, index, mdOpts, env) { const token = tokens[index]; const attrtype = token.attrGet('key'); // @ts-expect-error: forcing array -- technically not supposed to, but it works so nicely here (see note above) const filenames = token.attrGet('vals'); if (attrtype && filenames && opts.addAttr) { for (const filename of filenames) { opts.addAttr(env, attrtype, filename); } } return ''; } // render // example render output: // // <aside class="attrbox"> // <dl> // <dt>attrtype</dt> // <dd><a class="attr wiki attrtype doctype" href="/tests/fixtures/fname-a" data-href="/tests/fixtures/fname-a">title a</a></dd> // <dd><a class="attr wiki attrtype doctype" href="/tests/fixtures/fname-b" data-href="/tests/fixtures/fname-b">title b</a></dd> // <dd><a class="attr wiki attrtype doctype" href="/tests/fixtures/fname-c" data-href="/tests/fixtures/fname-c">title c</a></dd> // ... // </dl> // </aside> /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiattr_open(tokens, index, mdOpts, env) { return `<aside class="${opts.cssNames.attrbox}">\n<dl>\n`; } // attr : key : attrtype /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiattr_key(tokens, index, mdOpts, env) { const token = tokens[index]; const attrtype = token.attrGet('key'); // Check if there's a previous wikiattr_key (meaning we need to close the previous group div) let hasPriorKey = false; for (let i = index - 1; i >= 0; i--) { if (tokens[i].type === 'wikiattr_key') { hasPriorKey = true; break; } if (tokens[i].type === 'wikiattr_open') { break; } } const prefix = hasPriorKey ? `</div>\n<div class="${opts.cssNames.attrItem}">\n` : `<div class="${opts.cssNames.attrItem}">\n`; return attrtype ? `${prefix}<dt>${attrtype}</dt>\n` : `${prefix}<dt>attrtype error</dt>\n`; } // attr : val(s) : item(s) /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiattr_val(tokens, index, mdOpts, env) { // load const token = tokens[index]; if (token === null) { return '<dd>token error</dd>\n'; } const filename = token.attrGet('val'); if (filename === null) { return '<dd>filename error</dd>\n'; } const htmlHref = opts.resolveHtmlHref(env, filename); const htmlText = opts.resolveHtmlText(env, filename) ? opts.resolveHtmlText(env, filename) : filename; const doctype = opts.resolveDocType ? opts.resolveDocType(env, filename) : ''; // render // invalid if (htmlHref === undefined) { const wikitext = filename !== null ? filename : 'error'; return `<dd><a class="${opts.cssNames.attr} ${opts.cssNames.wiki} ${opts.cssNames.invalid}">${wikirefs.CONST.MARKER.OPEN}${wikitext}${wikirefs.CONST.MARKER.CLOSE}</a></dd>\n`; // valid } else { const attrtype = token.attrGet('key'); // css const cssClassArray = []; // 'attr' if (attrtype !== null && attrtype !== undefined) { cssClassArray.push(opts.cssNames.attr); } // 'wiki' cssClassArray.push(opts.cssNames.wiki); if (attrtype !== null && attrtype !== undefined) { const attrTypeSlug = wikirefs.slugify(attrtype); cssClassArray.push(opts.cssNames.reftype + attrTypeSlug); } // '<doctype>' if (doctype !== null && doctype !== undefined && doctype.length !== 0) { const docTypeSlug = wikirefs.slugify(doctype); cssClassArray.push(opts.cssNames.doctype + docTypeSlug); } const css = cssClassArray.join(' '); return `<dd><a class="${css}" href="${opts.baseUrl + htmlHref}" data-href="${opts.baseUrl + htmlHref}">${htmlText}</a></dd>\n`; } } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiattr_close(tokens, index, mdOpts, env) { delete env.cur_attr_type; return '</div>\n</dl>\n</aside>\n'; } }; const wikilinks = (md, opts) => { // rulers md.inline.ruler.after('link', 'wikilink', wikilink); // render md.renderer.rules.metadata_wikilink = metadata_wikilink; md.renderer.rules.wikilink_open = wikilink_open; md.renderer.rules.wikilink_body = wikilink_body; md.renderer.rules.wikilink_close = wikilink_close; // rulers function wikilink(state, silent) { var _match$; const srcText = state.src.substring(state.pos); // process match info const match = wikirefs.RGX.WIKI.LINK.exec(srcText); if (match == null || match.length < 1 || match.index !== 0) { return false; } // uncomment in case of skip wikiembeds // const embedChar: string = state.src.substring(state.pos - 1, state.pos); // if (embedChar === '!') { // return false; // } const matchText = match[0]; const linkTypeText = match[1] ? match[1].trim() : match[1]; const filenameText = match[2]; const headerText = match[3]; const labelText = (_match$ = match[4]) !== null && _match$ !== void 0 ? _match$ : null; // handle early kick-out if we've hit a stop-char early // // untyped if (!linkTypeText && srcText[0] !== '[') { return false; } // typed if (linkTypeText && srcText[0] !== ':') { return false; } // "Don't run any pairs in validation mode": // 'silent' is used when this rule is being checked against // in another rule to see whether or not the other rule should // kick out for this (or some other) one. return 'true' to // signify that the kick out should happen if (silent) { return false; } let token; // open // token = state.push('wikilink_open', 'wikilink', 0); token.attrSet('filename', filenameText); if (linkTypeText !== null && linkTypeText !== undefined && linkTypeText.length !== 0) { token.attrSet('linktype', linkTypeText); } if (headerText !== undefined && headerText.length > 0) { token.attrSet('header', headerText); } // body // token = state.push('wikilink_body', '', 0); token.attrSet('filename', filenameText); token.attrSet('matchText', matchText); if (headerText !== undefined && headerText.length > 0) { token.attrSet('header', headerText); } if (labelText) { token.attrSet('label', labelText); } // close // token = state.push('wikilink_close', 'wikilink', 0); // metadata if (opts.addLink) { token = state.push('metadata_wikilink', 'wikilink', 0); token.attrSet('linktype', linkTypeText); token.attrSet('filename', filenameText); if (headerText !== undefined && headerText.length > 0) { token.attrSet('header', headerText); } } // continue; increment position state.pos += matchText.length; return true; } // render /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function metadata_wikilink(tokens, index, mdOpts, env) { const token = tokens[index]; if (token === null) { return 'token error'; } // don't let 'linktype' be 'null' -- untyped wikilinks have empty strings for 'linktype' const linktype = token.attrGet('linktype') ? token.attrGet('linktype') : ''; const filename = token.attrGet('filename'); const header = token.attrGet('header'); if (linktype !== null && filename !== null && opts.addLink) { opts.addLink(env, linktype, filename, header !== null && header !== void 0 ? header : undefined); } return ''; } // render // typed // // <a class="wiki link type linktype doctype" href="/tests/fixtures/fname-a" data-href="/tests/fixtures/fname-a">title a</a> // // untyped // // <a class="wiki link doctype" href="/tests/fixtures/fname-a" data-href="/tests/fixtures/fname-a">title a</a> /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikilink_open(tokens, index, mdOpts, env) { // load const token = tokens[index]; const invalidOpen = `<a class="${opts.cssNames.wiki} ${opts.cssNames.link} ${opts.cssNames.invalid}">`; if (token === null) { return invalidOpen; } const filename = token.attrGet('filename'); const linktype = token.attrGet('linktype'); const header = token.attrGet('header'); if (filename === null) { return invalidOpen; } let htmlHref = opts.resolveHtmlHref(env, filename); const doctype = opts.resolveDocType ? opts.resolveDocType(env, filename) : ''; // render // invalid if (htmlHref === undefined) { return invalidOpen; // valid } else { if (header !== null && header.length > 0) { htmlHref = htmlHref + '#' + wikirefs.slugify(header); } // build css string const cssClassArray = []; // wikilink cssClassArray.push(opts.cssNames.wiki); cssClassArray.push(opts.cssNames.link); // linktype if (linktype !== null && linktype !== undefined && linktype.length !== 0) { cssClassArray.push(opts.cssNames.type); const linkTypeSlug = wikirefs.slugify(linktype); cssClassArray.push(opts.cssNames.reftype + linkTypeSlug); } // doctype if (doctype) { const docTypeSlug = wikirefs.slugify(doctype); cssClassArray.push(opts.cssNames.doctype + docTypeSlug); } const css = cssClassArray.join(' '); return `<a class="${css}" href="${opts.baseUrl + htmlHref}" data-href="${opts.baseUrl + htmlHref}">`; } } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikilink_body(tokens, index, mdOpts, env) { // load const token = tokens[index]; if (token === null) { return 'token error'; } const filename = token.attrGet('filename'); const labelText = token.attrGet('label'); const matchText = token.attrGet('matchText'); if (filename === null) { return 'filename error'; } const htmlHref = opts.resolveHtmlHref(env, filename); const htmlText = opts.resolveHtmlText(env, filename); // render // invalid if (!htmlHref && matchText) { token.content = matchText; // valid } else { // html text, order of precedence for (const content of [labelText, htmlText, filename]) { if (typeof content === 'string' && content.length > 0) { token.content = content; break; } } } return token.content; } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikilink_close(tokens, index, mdOpts, env) { return '</a>'; } }; function extname(fname) { const idx = fname.lastIndexOf('.'); return idx >= 0 ? fname.slice(idx) : ''; } const wikiembeds = (md, opts) => { // rulers // the inline rule runs before 'wikilink' so the leading `!` is consumed here. const wikiLinkRule = md.block.ruler.getRules('wikilink'); if (wikiLinkRule.length === 0) { md.inline.ruler.before('link', 'wikiembed', wikiembed); } else { md.inline.ruler.before('wikilink', 'wikiembed', wikiembed); } // core rule: hoist embeds (standalone + mid-text) out of paragraphs to block level md.core.ruler.push('wikiembed_hoist', wikiembed_hoist_rule); // render md.renderer.rules.metadata_wikiembed = metadata_wikiembed; md.renderer.rules.wikiembed_open = wikiembed_open; md.renderer.rules.wikiembed_close = wikiembed_close; // body md.renderer.rules.wikiembed_content_body_media = wikiembed_content_body_media; // markdown // title md.renderer.rules.wikiembed_title_open = wikiembed_title_open; md.renderer.rules.wikiembed_title_body = wikiembed_title_body; md.renderer.rules.wikiembed_title_close = wikiembed_title_close; // link md.renderer.rules.wikiembed_link_open = wikiembed_link_open; md.renderer.rules.wikiembed_link_body = wikiembed_link_body; md.renderer.rules.wikiembed_link_close = wikiembed_link_close; // content md.renderer.rules.wikiembed_content_open = wikiembed_content_open; md.renderer.rules.wikiembed_content_body_md = wikiembed_content_body_md; md.renderer.rules.wikiembed_content_close = wikiembed_content_close; // mid-text embed rendered as inline token (will be hoisted to block by core rule) md.renderer.rules.wikiembed_link_inline = wikiembed_link_inline; // rulers // inline rule: every embed (standalone or mid-text) -> a wikiembed_link_inline token. // the core hoist rule promotes it out of its paragraph to a block <article>/<figure>. function wikiembed(state, silent) { const srcText = state.src.substring(state.pos); // process match info const match = wikirefs.RGX.WIKI.EMBED.exec(srcText); if (match == null || match.length < 1 || match.index !== 0) { return false; } if (silent) { return false; } const matchText = match[0]; const filenameText = match[1]; const headerText = match[2]; const labelText = match[3]; const token = state.push('wikiembed_link_inline', '', 0); token.attrSet('filename', filenameText); token.content = matchText; if (headerText !== undefined && headerText.length > 0) { token.attrSet('header', headerText); } if (labelText !== undefined && labelText.length > 0) { token.attrSet('label', labelText); } // metadata is emitted by the core hoist rule (which owns the block article the embed // becomes); emitting it here too would double-fire addEmbed AND leave an orphaned // metadata token that the hoist wraps in a stray <p></p>. // continue; increment position state.pos += matchText.length; return true; } // core rule: hoist mid-text embeds out of paragraphs. // // Scans the flat token stream for paragraph_open / inline / paragraph_close triples. // When the inline token's children contain a wikiembed_link_inline, we split the // children at the embed, emit sibling block tokens: // [paragraph_open inline(before) paragraph_close]? // [wikiembed block tokens] // [paragraph_open inline(after) paragraph_close]? // Empty / whitespace-only text runs produce no <p>; punctuation-only runs do. function wikiembed_hoist_rule(state) { const tokens = state.tokens; let i = 0; while (i < tokens.length) { // look for paragraph triples if (tokens[i].type !== 'paragraph_open' || !tokens[i + 1] || tokens[i + 1].type !== 'inline' || !tokens[i + 2] || tokens[i + 2].type !== 'paragraph_close') { i++; continue; } const inlineTok = tokens[i + 1]; const children = inlineTok.children || []; // check if any child is a wikiembed_link_inline const embedIdx = children.findIndex(c => c.type === 'wikiembed_link_inline'); if (embedIdx < 0) { i += 3; continue; } // found one — build replacement tokens const replacement = []; // helper: build a paragraph token sequence from a child slice. // Trims surrounding softbreaks, and trims leading/trailing whitespace from // the boundary text tokens (the split at an embed boundary leaves trailing/leading // spaces on text tokens that would render as visible spurious spaces). const makePara = slice => { // trim leading/trailing softbreak tokens let start = 0; let end = slice.length; while (start < end && slice[start].type === 'softbreak') { start++; } while (end > start && slice[end - 1].type === 'softbreak') { end--; } // deep-copy the slice so we don't mutate the original children array const trimmed = slice.slice(start, end).map(t => { const copy = new state.Token(t.type, t.tag, t.nesting); copy.content = t.content; copy.children = t.children; copy.attrs = t.attrs ? t.attrs.slice() : null; copy.level = t.level; copy.map = t.map; copy.markup = t.markup; copy.meta = t.meta; copy.block = t.block; copy.hidden = t.hidden; copy.info = t.info; return copy; }); if (trimmed.length === 0) { return []; } // trim trailing whitespace from the last text token (split creates a trailing space) const last = trimmed[trimmed.length - 1]; if (last.type === 'text') { last.content = last.content.trimEnd(); } // trim leading whitespace from the first text token (split creates a leading space) const first = trimmed[0]; if (first.type === 'text') { first.content = first.content.trimStart(); } // check if all content is now empty/whitespace const isWhitespace = trimmed.every(t => (t.type === 'text' || t.type === 'softbreak') && t.content.trim() === ''); if (isWhitespace) { return []; } const pOpen = new state.Token('paragraph_open', 'p', 1); pOpen.map = tokens[i].map; pOpen.block = true; const pInline = new state.Token('inline', '', 0); pInline.content = ''; pInline.children = trimmed; pInline.block = true; const pClose = new state.Token('paragraph_close', 'p', -1); pClose.block = true; return [pOpen, pInline, pClose]; }; // helper: build the full block embed token sequence from a wikiembed_link_inline token const makeEmbedBlock = embedToken => { const filename = embedToken.attrGet('filename') || ''; const header = embedToken.attrGet('header'); const label = embedToken.attrGet('label'); const out = []; // open const openTok = new state.Token('wikiembed_open', 'wikiembed', 0); openTok.attrSet('filename', filename); if (header) { openTok.attrSet('header', header); } out.push(openTok); // body if (wikirefs.isMedia(filename)) { const mediaTok = new state.Token('wikiembed_content_body_media', 'wikiembed', 0); mediaTok.attrSet('filename', filename); if (header) { mediaTok.attrSet('header', header); } if (label) { mediaTok.attrSet('label', label); } out.push(mediaTok); } else { // title const titleOpen = new state.Token('wikiembed_title_open', 'wikiembed', 0); out.push(titleOpen); const titleBody = new state.Token('wikiembed_title_body', 'wikiembed', 0); titleBody.attrSet('filename', filename); if (header) { titleBody.attrSet('header', header); } out.push(titleBody); const titleClose = new state.Token('wikiembed_title_close', 'wikiembed', 0); out.push(titleClose); // link const linkOpen = new state.Token('wikiembed_link_open', 'wikiembed', 0); linkOpen.attrSet('filename', filename); if (header) { linkOpen.attrSet('header', header); } out.push(linkOpen); const linkBody = new state.Token('wikiembed_link_body', 'wikiembed', 0); linkBody.attrSet('filename', filename); out.push(linkBody); const linkClose = new state.Token('wikiembed_link_close', 'wikiembed', 0); out.push(linkClose); // content const contentOpen = new state.Token('wikiembed_content_open', 'wikiembed', 0); out.push(contentOpen); const contentBody = new state.Token('wikiembed_content_body_md', 'wikiembed', 0); contentBody.attrSet('filename', filename); if (header) { contentBody.attrSet('header', header); } out.push(contentBody); const contentClose = new state.Token('wikiembed_content_close', 'wikiembed', 0); out.push(contentClose); } // close const closeTok = new state.Token('wikiembed_close', 'wikiembed', 0); closeTok.attrSet('filename', filename); if (header) { closeTok.attrSet('header', header); } out.push(closeTok); // metadata if (opts.addEmbed) { const metaTok = new state.Token('metadata_wikiembed', 'wikiembed', 0); metaTok.attrSet('filename', filename); if (header) { metaTok.attrSet('header', header); } out.push(metaTok); } return out; }; // build: text-before para, embed block, text-after para const beforeChildren = children.slice(0, embedIdx); const afterChildren = children.slice(embedIdx + 1); replacement.push(...makePara(beforeChildren)); replacement.push(...makeEmbedBlock(children[embedIdx])); replacement.push(...makePara(afterChildren)); // splice the 3 original tokens out, insert replacement tokens.splice(i, 3, ...replacement); // don't advance i — re-scan from same position (might be another embed in afterChildren) } } // render // note (markdown) embeds -> a self-contained BLOCK: // // <article class="embed-wrapper"> // <div class="embed-title"> // <a class="wiki embed doctype" href="/tests/fixtures/embed-doc" data-href="/tests/fixtures/embed-doc"> // embedded document // </a> // </div> // <div class="embed-link"> // <a class="embed-link-icon" href="/tests/fixtures/embed-doc" data-href="/tests/fixtures/embed-doc"> // <i class="link-icon"></i> // </a> // </div> // <div class="embed-content"> // <p>Here is some content.</p> // </div> // </article> // media embeds (audio, img, video) -> a block <figure>: // // <figure class="embed-media" src="audio.mp3" alt="audio.mp3"> // <audio class="embed-audio" controls type="audio/mp3" src="/tests/fixtures/audio.mp3"></audio> // </figure> /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function metadata_wikiembed(tokens, index, mdOpts, env) { const token = tokens[index]; if (token === null) { return 'token error'; } // don't let 'linktype' be 'null' -- untyped wikiembeds have empty strings for 'linktype' const filename = token.attrGet('filename'); const header = token.attrGet('header'); if (filename !== null && opts.addEmbed) { opts.addEmbed(env, filename, header !== null && header !== void 0 ? header : undefined); } return ''; } // embed /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_open(tokens, index, mdOpts, env) { // load const token = tokens[index]; const filename = token.attrGet('filename'); if (filename === null) { return 'filename error'; } // render if (wikirefs.isMedia(filename)) { return ''; } else { return `<article class="${opts.cssNames.embedWrapper}">\n`; } } // title /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_title_open(tokens, index, mdOpts, env) { return `<div class="${opts.cssNames.embedTitle}">\n`; } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_title_body(tokens, index, mdOpts, env) { // load const token = tokens[index]; if (token === null) { return 'token error'; } const filename = token.attrGet('filename'); const header = token.attrGet('header'); if (filename === null) { return 'filename error'; } let htmlHref = opts.resolveHtmlHref(env, filename); const htmlText = opts.resolveHtmlText(env, filename) ? opts.resolveHtmlText(env, filename) : filename; const doctype = opts.resolveDocType ? opts.resolveDocType(env, filename) : ''; // render if (htmlHref === undefined) { return `<a class="${opts.cssNames.wiki} ${opts.cssNames.embed} ${opts.cssNames.invalid}">\n${htmlText}\n</a>\n`; } else { if (header !== null && header.length > 0) { htmlHref = htmlHref + '#' + wikirefs.slugify(header); } // build css string const cssClassArray = []; cssClassArray.push(opts.cssNames.wiki); cssClassArray.push(opts.cssNames.embed); // '<doctype>' if (doctype !== null && doctype !== undefined && doctype.length !== 0) { const docTypeSlug = wikirefs.slugify(doctype); cssClassArray.push(opts.cssNames.doctype + docTypeSlug); } const css = cssClassArray.join(' '); return `<a class="${css}" href="${opts.baseUrl + htmlHref}" data-href="${opts.baseUrl + htmlHref}">\n${htmlText}\n</a>\n`; } } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_title_close(tokens, index, mdOpts, env) { return '</div>\n'; } // link /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_link_open(tokens, index, mdOpts, env) { // load const token = tokens[index]; const invalidOpen = `<div class="${opts.cssNames.embedLink}">\n<a class="${opts.cssNames.embedLinkIcon} ${opts.cssNames.invalid}">\n`; if (token === null) { return invalidOpen; } const filename = token.attrGet('filename'); const header = token.attrGet('header'); if (filename === null) { return invalidOpen; } // render let htmlHref = opts.resolveHtmlHref(env, filename); // invalid if (htmlHref === undefined) { return invalidOpen; // valid } else { if (header !== null && header.length > 0) { htmlHref = htmlHref + '#' + wikirefs.slugify(header); } return `<div class="${opts.cssNames.embedLink}">\n<a class="${opts.cssNames.embedLinkIcon}" href="${opts.baseUrl + htmlHref}" data-href="${opts.baseUrl + htmlHref}">\n`; } } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_link_body(tokens, index, mdOpts, env) { const token = tokens[index]; // error if (token === null) { return 'token error'; } return `<i class="${opts.cssNames.linkIcon}"></i>\n`; } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_link_close(tokens, index, mdOpts, env) { return '</a>\n</div>\n'; } // content /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_content_open(tokens, index, mdOpts, env) { return `<div class="${opts.cssNames.embedContent}">\n`; } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_content_body_md(tokens, index, mdOpts, env) { // load const token = tokens[index]; if (token === null) { return 'token error'; } const filename = token.attrGet('filename'); if (filename === null) { return opts.embeds.errorContent + '\'' + filename + '\''; } const hText = token.attrGet('header'); const htmlContent = opts.resolveEmbedContent(env, filename, hText !== null && hText !== void 0 ? hText : undefined); // render token.content = htmlContent ? htmlContent : opts.embeds.errorContent + '\'' + filename + '\''; // normalize to exactly one trailing newline (resolved markdown content already ends in // `\n`; a raw sentinel like 'cycle detected' does not) — no spurious blank before </div>. return token.content.replace(/\n+$/, '') + '\n'; } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_content_body_media(tokens, index, mdOpts, env) { // load const token = tokens[index]; if (token === null) { return 'token error'; } const filename = token.attrGet('filename'); if (filename === null) { return 'filename error'; } const filenameSlug = filename.trim().toLowerCase().replace(/ /g, '-'); const htmlHref = opts.resolveHtmlHref(env, filename); const mediaExt = extname(filename).toLowerCase(); const mime = extname(filename).replace('.', '').toLowerCase(); const label = token.attrGet('label'); const caption = label ? `<figcaption>${label}</figcaption>\n` : ''; // render: open <figure> let content = `<figure class="${opts.cssNames.embedMedia}" src="${filenameSlug}" alt="${filenameSlug}">\n`; // PDF if (mediaExt === wikirefs.CONST.EXTS.PDF) { const pdfSrc = htmlHref !== null && htmlHref !== void 0 ? htmlHref : ''; content += `<object class="${opts.cssNames.embedPDF}" data="${pdfSrc}" type="application/pdf">\n`; content += `<a href="${pdfSrc}">${filenameSlug}</a>\n`; content += '</object>\n'; // audio } else if (wikirefs.CONST.EXTS.AUD.has(mediaExt)) { content += htmlHref ? `<audio class="${opts.cssNames.embedAudio}" controls type="audio/${mime}" src="${htmlHref}"></audio>\n` : `<audio class="${opts.cssNames.embedAudio}" controls type="audio/${mime}"></audio>\n`; // image — add alt attribute } else if (wikirefs.CONST.EXTS.IMG.has(mediaExt)) { content += htmlHref ? `<img class="${opts.cssNames.embedImage}" src="${htmlHref}" alt="${filenameSlug}">\n` : `<img class="${opts.cssNames.embedImage}" alt="${filenameSlug}">\n`; // video } else if (wikirefs.CONST.EXTS.VID.has(mediaExt)) { content += htmlHref ? `<video class="${opts.cssNames.embedVideo}" controls type="video/${mime}" src="${htmlHref}"></video>\n` : `<video class="${opts.cssNames.embedVideo}" controls type="video/${mime}"></video>\n`; } else { // note: this is probably not technically possible (due to 'wikirefs.isMedia()' check) content += 'media error\n'; } // caption (media only — before closing </figure>) content += caption; content += '</figure>\n'; token.content = content; return token.content; } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_content_close(tokens, index, mdOpts, env) { return '</div>\n'; } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_close(tokens, index, mdOpts, env) { // load const token = tokens[index]; const filename = token.attrGet('filename'); if (filename === null) { return 'filename error'; } // render: the media <figure> is fully emitted (open + media + caption + close) by // wikiembed_content_body_media, so nothing to close here. note embeds close <article>. if (wikirefs.isMedia(filename)) { return ''; } else { return '</article>\n'; } } // mid-text embed -> hoisted to block article by core rule; this renderer is a // fallback that should not be reached in normal flow. /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function wikiembed_link_inline(tokens, index, mdOpts, env) { // This token is consumed by the core hoist rule before rendering; if we end up // here it means the embed appears outside a paragraph (e.g. already rendered by // a nested content path). Render it as a raw embed block. const token = tokens[index]; if (token === null) { return 'token error'; } const filename = token.attrGet('filename'); if (filename === null) { return 'filename error'; } const header = token.attrGet('header'); token.attrGet('label'); let htmlHref = opts.resolveHtmlHref(env, filename); const htmlText = opts.resolveHtmlText(env, filename) ? opts.resolveHtmlText(env, filename) : filename; const doctype = opts.resolveDocType ? opts.resolveDocType(env, filename) : ''; // invalid -> raw display (matches wikilink) if (htmlHref === undefined) { return `<a class="${opts.cssNames.wiki} ${opts.cssNames.link} ${opts.cssNames.invalid}">${token.content}</a>`; } // valid -> wiki link (fallback; core rule should have hoisted this to block) if (header !== null && header.length > 0) { htmlHref = htmlHref + '#' + wikirefs.slugify(header); } const cssClassArray = []; cssClassArray.push(opts.cssNames.wiki); cssClassArray.push(opts.cssNames.link); if (doctype !== null && doctype !== undefined && doctype.length !== 0) { const docTypeSlug = wikirefs.slugify(doctype); cssClassArray.push(opts.cssNames.doctype + docTypeSlug); } const css = cssClassArray.join(' '); return `<a class="${css}" href="${opts.baseUrl + htmlHref}" data-href="${opts.baseUrl + htmlHref}">${htmlText}</a>`; } }; // import // export function wikirefs_plugin(md, opts) { // opts const defaults = { resolveHtmlText: (env, fname) => fname.replace(/-/g, ' '), resolveHtmlHref: (env, fname) => { const extname = wikirefs.isMedia(fname) ? fname.slice(fname.lastIndexOf('.')) : ''; fname = fname.replace(extname, ''); return '/' + wikirefs.slugify(fname) + extname; }, resolveEmbedContent: (env, fname, hText) => fname + ' content', baseUrl: '', cssNames: { // wiki wiki: 'wiki', invalid: 'invalid', // kinds attr: 'attr', link: 'link', type: 'type', embed: 'embed', // types reftype: 'reftype__', doctype: 'doctype__', // attr attrbox: 'attrbox', attrItem: 'attr-item', // embed embedWrapper: 'embed-wrapper', embedTitle: 'embed-title', embedLink: 'embed-link', embedContent: 'embed-content', embedLinkIcon: 'embed-link-icon', linkIcon: 'link-icon', embedMedia: 'embed-media', embedAudio: 'embed-audio', embedImage: 'embed-image', embedPDF: 'embed-pdf', embedVideo: 'embed-video' }, attrs: { enable: true, render: true, title: 'Attributes' }, links: { enable: true }, embeds: { enable: true, title: 'Embed Content', errorContent: 'Error: Content not found for ' } }; const fullOpts = defu(opts, defaults); // by order of execution if (fullOpts.prepFile) { prep_file(md, fullOpts); } if (fullOpts.attrs && fullOpts.attrs.enable) { wikiattrs(md, fullOpts); } if (fullOpts.links && fullOpts.links.enable) { wikilinks(md, fullOpts); } if (fullOpts.embeds && fullOpts.embeds.enable) { wikiembeds(md, fullOpts); } } export { wikirefs_plugin as default }; //# sourceMappingURL=index.esm.js.map