diff --git a/packages/nuemark/src/parse-blocks.js b/packages/nuemark/src/parse-blocks.js index 9aa1ef33..5cdd5e34 100644 --- a/packages/nuemark/src/parse-blocks.js +++ b/packages/nuemark/src/parse-blocks.js @@ -114,7 +114,7 @@ export function parseBlocks(lines, capture) { // tag if (c == '[' && trimmed.endsWith(']') && !trimmed.includes('][')) { const tag = parseTag(line.slice(1, -1)) - block = { is_tag: true, ...tag, body: [] } + block = { is_tag: true, ...tag, name: tag.name || 'div', body: [] } return blocks.push(block) } @@ -182,7 +182,9 @@ function processNestedBlocks(block, capture) { const body = block.body.join('\n') try { - if (body && name && isYAML(body.trim())) { + // TODO: add additional check for native html tags + // maybe new syntax? e.g. `[yaml-tag]:\n\thi` (note colon) + if (body && isYAML(body.trim())) { let data = parseYAML(body) if (Array.isArray(data)) data = { items: data } Object.assign(block.data, data) diff --git a/packages/nuemark/src/parse-inline.js b/packages/nuemark/src/parse-inline.js index f32acc53..3b1e816f 100644 --- a/packages/nuemark/src/parse-inline.js +++ b/packages/nuemark/src/parse-inline.js @@ -78,7 +78,7 @@ const PARSERS = [ // parse tag const tag = parseTag(str.slice(1, i).trim()) const { name } = tag - const is_footnote = name[0] == '^' + const is_footnote = name && name[0] == '^' const end = i + 1 // footnote? @@ -88,7 +88,7 @@ const PARSERS = [ } // normal tag - if (name == '!' || isValidName(name)) return { is_tag: true, ...tag, end } + if (!name || name == '!' || isValidName(name)) return { is_inline: true, is_tag: true, ...tag, name: tag.name || 'span', end } return { text: c } } diff --git a/packages/nuemark/src/render-tag.js b/packages/nuemark/src/render-tag.js index eef01463..21716c77 100644 --- a/packages/nuemark/src/render-tag.js +++ b/packages/nuemark/src/render-tag.js @@ -6,6 +6,17 @@ import { elem } from './render-blocks.js' import { readFileSync } from 'node:fs' import { join } from 'node:path' +// mostly the same as first block from <../../nuejs/src/fn.js>, but excludes: html, head +const HTML_TAGS = 'a abbr acronym address applet area article aside audio b base basefont bdi bdo big\ + blockquote body br button canvas caption center circle cite clipPath code col colgroup data datalist\ + dd defs del details dfn dialog dir div dl dt ellipse em embed fieldset figcaption figure font footer\ + foreignObject form frame frameset g header hgroup h1 h2 h3 h4 h5 h6 hr i iframe image img\ + input ins kbd keygen label legend li line link main map mark marker mask menu menuitem meta meter\ + nav noframes noscript object ol optgroup option output p param path pattern picture polygon polyline\ + pre progress q rect rp rt ruby s samp script section select small source span strike strong style sub\ + summary sup svg switch symbol table tbody td template text textarea textPath tfoot th thead time\ + title tr track tspan tt u ul use var video wbr'.split(' ') + // built-in tags const TAGS = { @@ -22,22 +33,14 @@ const TAGS = { }, block() { - const { render, attr, blocks } = this + const { render, attr, data, blocks, name } = this const divs = sectionize(blocks) const html = !divs || !divs[1] ? render(blocks) : divs.map(blocks => elem('div', render(blocks))).join('\n') - return elem(attr.popover ? 'dialog' : 'div', attr, html) - }, - - - button(data) { - const { href } = data - const label = this.renderInline(data.label || data._) || this.innerHTML || '' - - return href ? elem('a', { ...this.attr, href, role: 'button' }, label) : - elem('button', this.attr, label) + if (this.to_block) Object.assign(attr, data) + return elem(attr.popover ? 'dialog' : name, attr, html) }, define() { @@ -52,7 +55,6 @@ const TAGS = { return html && elem('dl', this.attr, html.join('\n')) }, - image() { const { attr, data } = this const { caption, href, loading = 'lazy' } = data @@ -74,6 +76,15 @@ const TAGS = { return elem('figure', attr, img) }, + inline() { + const { name, attr, data, opts } = this + + const content = data._ + delete data._ + if (this.to_inline) Object.assign(attr, data) + + return elem(name, attr, this.renderInline(content, opts)) + }, list() { const items = this.sections || getListItems(this.blocks) @@ -109,7 +120,6 @@ const TAGS = { return elem('video', attr, this.innerHTML) }, - // shortcut '!': function() { const tag = getMimeType(this.data._).startsWith('video') ? TAGS.video : TAGS.image @@ -142,9 +152,22 @@ export function renderIcon(name, symbol, icon_dir) { export function renderTag(tag, opts = {}) { const tags = { ...TAGS, ...opts.tags } - const fn = tags[tag.name || 'block'] + const tag_fn = tag.to_block ? 'block' : tag.to_inline ? 'inline' : tag.name + const fn = tags[tag_fn] + + if (!fn) { + // native html tags + if (HTML_TAGS.includes(tag.name)) { + // inline / block without blocks + if (tag.is_inline || !tag.blocks?.length) tag.to_inline = true + // block + else tag.to_block = true + + return renderTag(tag) + } - if (!fn) return renderIsland(tag, opts.data) + return renderIsland(tag, opts.data) + } const data = { ...opts.data, ...extractData(tag.data, opts.data) } @@ -307,4 +330,4 @@ export function parseTable(lines) { }) return { rows, ...specs } -} \ No newline at end of file +} diff --git a/packages/nuemark/test/block.test.js b/packages/nuemark/test/block.test.js index bd43c492..844f5479 100644 --- a/packages/nuemark/test/block.test.js +++ b/packages/nuemark/test/block.test.js @@ -30,6 +30,60 @@ test('nested lists', () => { }) +test('block html tag including non-html tag', () => { + const { blocks } = parseBlocks(['[section.hi]', ' content', ' [subtag "data"]']) + const parent = blocks[0] + expect(blocks.length).toBe(1) + expect(parent.is_tag).toBe(true) + expect(parent.attr.class).toBe('hi') + expect(parent.blocks.length).toBe(2) + const children = blocks[0].blocks + expect(children[0].is_content).toBe(true) + expect(children[1].is_tag).toBe(true) + expect(children[1].data).toEqual({ _: "data" }) + + const html = renderBlocks(blocks) + expect(html).toStartWith('

content

') + expect(html).toInclude('') + expect(html).toEndWith('
') +}) + +test('block html tag without children with content', () => { + const { blocks } = parseBlocks(['[section "content"]', 'no content']) + expect(blocks.length).toBe(2) + + const html = renderBlocks(blocks) + expect(html).toBe('
content
\n

no content

') +}) + +test('block html tag with starting ul', () => { + const { blocks } = parseBlocks(['[div]', ' - hi', ' - hello']) + expect(blocks.length).toBe(1) + expect(blocks[0].blocks.length).toBe(1) + + const html = renderBlocks(blocks) + expect(html).toBe('
') +}) + +test('inlined block html with attrs', () => { + const { blocks } = parseBlocks(['[div myattr="data" "content"]']) + console.log(blocks) + expect(blocks.length).toBe(1) + expect(blocks[0].data).toEqual({ myattr: 'data', _: 'content' }) + + const html = renderBlocks(blocks) + expect(html).toBe('
content
') +}) + +test('block html with attrs', () => { + const { blocks } = parseBlocks(['[div myattr="data" "not content"]', ' content']) + expect(blocks.length).toBe(1) + expect(blocks[0].data).toEqual({ myattr: 'data', _: "not content" }) + + const html = renderBlocks(blocks) + expect(html).toBe('

content

') +}) + test('nested tag data', () => { const { blocks } = parseBlocks(['[hello]', '', '', ' foo: bar', '', ' bro: 10']) expect(blocks[0].data).toEqual({ foo: "bar", bro: 10 }) diff --git a/packages/nuemark/test/inline.test.js b/packages/nuemark/test/inline.test.js index 7524c912..72c1d0d9 100644 --- a/packages/nuemark/test/inline.test.js +++ b/packages/nuemark/test/inline.test.js @@ -217,6 +217,42 @@ test('parse simple image', () => { expect(img.href).toBe('yo.svg') }) +// anonymous tag +test('inline span', () => { + const html = renderInline('hello [.green "world"]!') + expect(html).toBe('hello world!') +}) + +test('empty inline span', () => { + const html = renderInline('[.myclass#myid]') + expect(html).toStartWith('') +}) + +test('inline html with attrs', () => { + const md = '[span myattr="data" "content"]' + const [tag] = parseInline(md) + expect(tag.data).toEqual({ myattr: 'data', _: 'content' }) + + const html = renderInline(md) + expect(html).toBe('content') +}) + +// named default html tag +test('inline html tag', () => { + const html = renderInline('[b "*content*"]') + expect(html).toBe('content') +}) + +test('empty inline html tag', () => { + const html = renderInline('[del.pink.border#myid]') + expect(html).toStartWith('') +}) // parse tags and args test('inline tag', () => { @@ -228,6 +264,7 @@ test('inline tag with reflink', () => { const els = parseInline('[tip] and [link][foo]') const [ tag, and, link] = els expect(tag.is_tag).toBeTrue() + expect(tag.name).toBe('tip') expect(link.is_reflink).toBeTrue() }) diff --git a/packages/nuemark/test/tag.test.js b/packages/nuemark/test/tag.test.js index 69fddc2e..5d9581fe 100644 --- a/packages/nuemark/test/tag.test.js +++ b/packages/nuemark/test/tag.test.js @@ -213,12 +213,12 @@ test('[table] empty cells', () => { test('[button] inline label', () => { const html = renderLines(['[button href="/" "Hey, *world*"]']) - expect(html).toBe('Hey, world') + expect(html).toBe('') }) test('[button] nested label', () => { const html = renderLines(['[button href=/]', ' ![](/joku.png)']) - expect(html).toStartWith('

{ test('[svg] nested in [button]', () => { const html = renderLines(['[button href="/"]', ` [svg ${svgpath}] *Yo*`]) - expect(html).toBe(' Yo') + expect(html).toBe('') })