From c607d5955d35f8b28ccf61347e5f83b2ef91abf9 Mon Sep 17 00:00:00 2001 From: nobkd <44443899+nobkd@users.noreply.github.com> Date: Tue, 28 Jan 2025 01:11:10 +0100 Subject: [PATCH 1/7] fix toc cropped, fix heading not hidden in zen mode --- packages/nuemark/src/parse-inline.js | 4 +++- packages/nuemark/src/render-inline.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/nuemark/src/parse-inline.js b/packages/nuemark/src/parse-inline.js index f32acc53..c3404565 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? @@ -89,6 +89,8 @@ const PARSERS = [ // normal tag if (name == '!' || isValidName(name)) return { is_tag: true, ...tag, end } + // span + if (!name) return { is_span: true, ...tag, end } return { text: c } } diff --git a/packages/nuemark/src/render-inline.js b/packages/nuemark/src/render-inline.js index cc41e959..ce9131c7 100644 --- a/packages/nuemark/src/render-inline.js +++ b/packages/nuemark/src/render-inline.js @@ -8,6 +8,7 @@ export function renderToken(token, opts = {}) { const { text } = token return text ? text : + token.is_span ? elem('span', token.attr, renderInline(token.data?._)) : token.is_format ? formatText(token, opts) : token.is_var ? renderVariable(token.name, data) : token.is_image ? renderImage(token) : From 534b1389d4b8df4cea54b6ca75b4bb0374b7c667 Mon Sep 17 00:00:00 2001 From: nobkd <44443899+nobkd@users.noreply.github.com> Date: Fri, 31 Jan 2025 03:15:10 +0100 Subject: [PATCH 2/7] inline span test --- packages/nuemark/test/inline.test.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/nuemark/test/inline.test.js b/packages/nuemark/test/inline.test.js index 7524c912..ce2e9ba8 100644 --- a/packages/nuemark/test/inline.test.js +++ b/packages/nuemark/test/inline.test.js @@ -217,6 +217,23 @@ test('parse simple image', () => { expect(img.href).toBe('yo.svg') }) +test('inline span', () => { + const md = 'hello [.green "world"]!' + const [text, span] = parseInline(md) + expect(span.is_span).toBeTrue() + + const result = renderInline(md) + expect(result).toBe('hello world!') +}) + +test('empty inline span', () => { + const result = renderInline('[.myclass#myid]') + expect(result).toStartWith('') +}) + // parse tags and args test('inline tag', () => { From eb939074e567c6bf5c26486f84a032f20f34be77 Mon Sep 17 00:00:00 2001 From: nobkd <44443899+nobkd@users.noreply.github.com> Date: Mon, 3 Feb 2025 02:57:14 +0100 Subject: [PATCH 3/7] feat: block and inline native html tags --- packages/nuemark/src/parse-inline.js | 4 +--- packages/nuemark/src/render-inline.js | 1 - packages/nuemark/src/render-tag.js | 31 +++++++++++++++++++++---- packages/nuemark/test/block.test.js | 26 +++++++++++++++++++++ packages/nuemark/test/inline.test.js | 33 ++++++++++++++++++--------- 5 files changed, 76 insertions(+), 19 deletions(-) diff --git a/packages/nuemark/src/parse-inline.js b/packages/nuemark/src/parse-inline.js index c3404565..3b1e816f 100644 --- a/packages/nuemark/src/parse-inline.js +++ b/packages/nuemark/src/parse-inline.js @@ -88,9 +88,7 @@ const PARSERS = [ } // normal tag - if (name == '!' || isValidName(name)) return { is_tag: true, ...tag, end } - // span - if (!name) return { is_span: 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-inline.js b/packages/nuemark/src/render-inline.js index ce9131c7..cc41e959 100644 --- a/packages/nuemark/src/render-inline.js +++ b/packages/nuemark/src/render-inline.js @@ -8,7 +8,6 @@ export function renderToken(token, opts = {}) { const { text } = token return text ? text : - token.is_span ? elem('span', token.attr, renderInline(token.data?._)) : token.is_format ? formatText(token, opts) : token.is_var ? renderVariable(token.name, data) : token.is_image ? renderImage(token) : diff --git a/packages/nuemark/src/render-tag.js b/packages/nuemark/src/render-tag.js index eef01463..0b63c89a 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,13 +33,13 @@ const TAGS = { }, block() { - const { render, attr, blocks } = this + const { render, attr, 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) + return elem(attr.popover ? 'dialog' : name || 'div', attr, html) }, @@ -142,9 +153,21 @@ export function renderIcon(name, symbol, icon_dir) { export function renderTag(tag, opts = {}) { const tags = { ...TAGS, ...opts.tags } - const fn = tags[tag.name || 'block'] + const fn = tags[!tag.is_block && tag.name || 'block'] + + if (!fn) { + // native html tags + if (HTML_TAGS.includes(tag.name)) { + // inline / block without blocks, but with '_' data + if (tag.is_inline || (!tag.blocks?.length && tag.data?._)) return elem(tag.name, tag.attr, renderInline(tag.data?._, opts)) - if (!fn) return renderIsland(tag, opts.data) + // block + tag.is_block = true + return renderTag(tag) + } + + return renderIsland(tag, opts.data) + } const data = { ...opts.data, ...extractData(tag.data, opts.data) } diff --git a/packages/nuemark/test/block.test.js b/packages/nuemark/test/block.test.js index bd43c492..d21cf221 100644 --- a/packages/nuemark/test/block.test.js +++ b/packages/nuemark/test/block.test.js @@ -30,6 +30,32 @@ 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('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 ce2e9ba8..4533324c 100644 --- a/packages/nuemark/test/inline.test.js +++ b/packages/nuemark/test/inline.test.js @@ -217,23 +217,33 @@ test('parse simple image', () => { expect(img.href).toBe('yo.svg') }) +// anonymous tag test('inline span', () => { - const md = 'hello [.green "world"]!' - const [text, span] = parseInline(md) - expect(span.is_span).toBeTrue() - - const result = renderInline(md) - expect(result).toBe('hello world!') + const html = renderInline('hello [.green "world"]!') + expect(html).toBe('hello world!') }) test('empty inline span', () => { - const result = renderInline('[.myclass#myid]') - expect(result).toStartWith('') + const html = renderInline('[.myclass#myid]') + expect(html).toStartWith('') }) +// 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', () => { @@ -245,6 +255,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() }) From 98a61814d1cf15b9e16d754592ce8a8a97e5062f Mon Sep 17 00:00:00 2001 From: nobkd <44443899+nobkd@users.noreply.github.com> Date: Mon, 3 Feb 2025 03:37:19 +0100 Subject: [PATCH 4/7] Simplify check for inline rendering --- packages/nuemark/src/render-tag.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nuemark/src/render-tag.js b/packages/nuemark/src/render-tag.js index 0b63c89a..f146afb1 100644 --- a/packages/nuemark/src/render-tag.js +++ b/packages/nuemark/src/render-tag.js @@ -158,8 +158,8 @@ export function renderTag(tag, opts = {}) { if (!fn) { // native html tags if (HTML_TAGS.includes(tag.name)) { - // inline / block without blocks, but with '_' data - if (tag.is_inline || (!tag.blocks?.length && tag.data?._)) return elem(tag.name, tag.attr, renderInline(tag.data?._, opts)) + // inline / block without blocks + if (tag.is_inline || !tag.blocks?.length) return elem(tag.name, tag.attr, renderInline(tag.data?._, opts)) // block tag.is_block = true @@ -330,4 +330,4 @@ export function parseTable(lines) { }) return { rows, ...specs } -} \ No newline at end of file +} From dcce908cf1df54dde054685d5d20f07ae3231ab5 Mon Sep 17 00:00:00 2001 From: nobkd <44443899+nobkd@users.noreply.github.com> Date: Mon, 3 Feb 2025 20:10:05 +0100 Subject: [PATCH 5/7] append data attrs on html tags --- packages/nuemark/src/render-tag.js | 22 +++++++++++++++++----- packages/nuemark/test/block.test.js | 28 ++++++++++++++++++++++++++++ packages/nuemark/test/inline.test.js | 9 +++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/packages/nuemark/src/render-tag.js b/packages/nuemark/src/render-tag.js index f146afb1..0cf3e7ba 100644 --- a/packages/nuemark/src/render-tag.js +++ b/packages/nuemark/src/render-tag.js @@ -33,12 +33,14 @@ const TAGS = { }, block() { - const { render, attr, blocks, name } = 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') + Object.assign(attr, data) + return elem(attr.popover ? 'dialog' : name || 'div', attr, html) }, @@ -85,6 +87,15 @@ const TAGS = { return elem('figure', attr, img) }, + inline() { + const { name, attr, data, opts } = this + + const content = data._ + delete data._ + Object.assign(attr, data) + + return elem(name, attr, this.renderInline(content, opts)) + }, list() { const items = this.sections || getListItems(this.blocks) @@ -153,16 +164,17 @@ export function renderIcon(name, symbol, icon_dir) { export function renderTag(tag, opts = {}) { const tags = { ...TAGS, ...opts.tags } - const fn = tags[!tag.is_block && tag.name || 'block'] + const tag_fn = !tag.name || 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) return elem(tag.name, tag.attr, renderInline(tag.data?._, opts)) - + if (tag.is_inline || !tag.blocks?.length) tag.to_inline = true // block - tag.is_block = true + else tag.to_block = true + return renderTag(tag) } diff --git a/packages/nuemark/test/block.test.js b/packages/nuemark/test/block.test.js index d21cf221..432edeff 100644 --- a/packages/nuemark/test/block.test.js +++ b/packages/nuemark/test/block.test.js @@ -56,6 +56,34 @@ test('block html tag without children with content', () => { expect(html).toBe('
content
\n

no content

') }) +test.skip('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 4533324c..72c1d0d9 100644 --- a/packages/nuemark/test/inline.test.js +++ b/packages/nuemark/test/inline.test.js @@ -231,6 +231,15 @@ test('empty inline span', () => { expect(html).toEndWith('>') }) +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*"]') From dc755c0ba5b69927b590fe9a030dae0ee4f8cb76 Mon Sep 17 00:00:00 2001 From: nobkd <44443899+nobkd@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:15:22 +0100 Subject: [PATCH 6/7] fix: unnamed block tag now defaults to div, don't skip list issue test --- packages/nuemark/src/parse-blocks.js | 6 ++++-- packages/nuemark/src/render-tag.js | 12 ++++-------- packages/nuemark/test/block.test.js | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) 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/render-tag.js b/packages/nuemark/src/render-tag.js index 0cf3e7ba..4f7869c8 100644 --- a/packages/nuemark/src/render-tag.js +++ b/packages/nuemark/src/render-tag.js @@ -39,12 +39,10 @@ const TAGS = { const html = !divs || !divs[1] ? render(blocks) : divs.map(blocks => elem('div', render(blocks))).join('\n') - Object.assign(attr, data) - - return elem(attr.popover ? 'dialog' : name || 'div', attr, html) + if (this.to_block) Object.assign(attr, data) + return elem(attr.popover ? 'dialog' : name, attr, html) }, - button(data) { const { href } = data const label = this.renderInline(data.label || data._) || this.innerHTML || '' @@ -65,7 +63,6 @@ const TAGS = { return html && elem('dl', this.attr, html.join('\n')) }, - image() { const { attr, data } = this const { caption, href, loading = 'lazy' } = data @@ -92,7 +89,7 @@ const TAGS = { const content = data._ delete data._ - Object.assign(attr, data) + if (this.to_inline) Object.assign(attr, data) return elem(name, attr, this.renderInline(content, opts)) }, @@ -131,7 +128,6 @@ const TAGS = { return elem('video', attr, this.innerHTML) }, - // shortcut '!': function() { const tag = getMimeType(this.data._).startsWith('video') ? TAGS.video : TAGS.image @@ -164,7 +160,7 @@ export function renderIcon(name, symbol, icon_dir) { export function renderTag(tag, opts = {}) { const tags = { ...TAGS, ...opts.tags } - const tag_fn = !tag.name || tag.to_block ? 'block' : tag.to_inline ? 'inline' : tag.name + const tag_fn = tag.to_block ? 'block' : tag.to_inline ? 'inline' : tag.name const fn = tags[tag_fn] if (!fn) { diff --git a/packages/nuemark/test/block.test.js b/packages/nuemark/test/block.test.js index 432edeff..844f5479 100644 --- a/packages/nuemark/test/block.test.js +++ b/packages/nuemark/test/block.test.js @@ -56,7 +56,7 @@ test('block html tag without children with content', () => { expect(html).toBe('
content
\n

no content

') }) -test.skip('block html tag with starting ul', () => { +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) From fb2d3cd1fc12c34ea024fe04105b629529b119d7 Mon Sep 17 00:00:00 2001 From: nobkd <44443899+nobkd@users.noreply.github.com> Date: Sat, 15 Feb 2025 05:48:09 +0100 Subject: [PATCH 7/7] use default rendering for button --- packages/nuemark/src/render-tag.js | 8 -------- packages/nuemark/test/tag.test.js | 6 +++--- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/nuemark/src/render-tag.js b/packages/nuemark/src/render-tag.js index 4f7869c8..21716c77 100644 --- a/packages/nuemark/src/render-tag.js +++ b/packages/nuemark/src/render-tag.js @@ -43,14 +43,6 @@ const TAGS = { return elem(attr.popover ? 'dialog' : name, 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) - }, - define() { const html = this.sections?.map((blocks, i) => { const { attr, text } = blocks[0] 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('') })