Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nuemark): inline span support, inline and block html nuemark tag support #470

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/nuemark/src/parse-blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions packages/nuemark/src/parse-inline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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 }
}
Expand Down
47 changes: 39 additions & 8 deletions packages/nuemark/src/render-tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -22,16 +33,16 @@ 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)
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 || ''
Expand All @@ -52,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
Expand All @@ -74,6 +84,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)
Expand Down Expand Up @@ -109,7 +128,6 @@ const TAGS = {
return elem('video', attr, this.innerHTML)
},


// shortcut
'!': function() {
const tag = getMimeType(this.data._).startsWith('video') ? TAGS.video : TAGS.image
Expand Down Expand Up @@ -142,9 +160,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) }

Expand Down Expand Up @@ -307,4 +338,4 @@ export function parseTable(lines) {
})

return { rows, ...specs }
}
}
54 changes: 54 additions & 0 deletions packages/nuemark/test/block.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,60 @@
})


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('<section class="hi"><p>content</p>')
expect(html).toInclude('<subtag custom="subtag"><script type="application/json">{"_":"data"}</script></subtag>')
expect(html).toEndWith('</section>')
})

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('<section>content</section>\n<p>no content</p>')
})

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)

Check failure on line 62 in packages/nuemark/test/block.test.js

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, bun)

TypeError: undefined is not an object (evaluating 'blocks[0].blocks.length')

at <anonymous> (/home/runner/work/nue/nue/packages/nuemark/test/block.test.js:62:20)

const html = renderBlocks(blocks)
expect(html).toBe('<div><ul><li>hi</li>\n<li>hello</li></ul></div>')
})

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('<div myattr="data">content</div>')
})

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('<div myattr="data" _="not content"><p>content</p></div>')
})

test('nested tag data', () => {
const { blocks } = parseBlocks(['[hello]', '', '', ' foo: bar', '', ' bro: 10'])
expect(blocks[0].data).toEqual({ foo: "bar", bro: 10 })
Expand Down
37 changes: 37 additions & 0 deletions packages/nuemark/test/inline.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <span class="green">world</span>!')
})

test('empty inline span', () => {
const html = renderInline('[.myclass#myid]')
expect(html).toStartWith('<span ')
expect(html).toInclude('class="myclass"')
expect(html).toInclude('id="myid"')
expect(html).toEndWith('></span>')
})

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('<span myattr="data">content</span>')
})

// named default html tag
test('inline html tag', () => {
const html = renderInline('[b "*content*"]')
expect(html).toBe('<b><em>content</em></b>')
})

test('empty inline html tag', () => {
const html = renderInline('[del.pink.border#myid]')
expect(html).toStartWith('<del ')
expect(html).toInclude('id="myid"')
expect(html).toInclude('class="pink border"')
expect(html).toEndWith('></del>')
})

// parse tags and args
test('inline tag', () => {
Expand All @@ -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()
})

Expand Down
Loading