Comparing version 0.1.0-dev to 0.1.0
10
index.js
import { render } from './src/render.js' | ||
import { renderLines } from './src/render.js' | ||
@@ -7,3 +7,3 @@ | ||
export function nuemark(str, opts) { | ||
return render(str.split('\n'), opts) | ||
return renderLines(str.split('\n'), opts) | ||
} | ||
@@ -13,6 +13,10 @@ | ||
export function nuemarkdown(str, opts) { | ||
delete opts?.data?.draw_sections | ||
return nuemark(str, opts).html | ||
} | ||
// returns { meta, sections, headings, links } | ||
export { parsePage } from './src/parse.js' | ||
export { renderPage } from './src/render.js' | ||
export { parsePage } from './src/parse.js' |
{ | ||
"name": "nuemark", | ||
"version": "0.1.0-dev", | ||
"description": "Markdown variant for rich, interactive content", | ||
"version": "0.1.0", | ||
"description": "Markdown dialect for rich, interactive web content", | ||
"homepage": "https://nuejs.org", | ||
@@ -13,2 +13,5 @@ "license": "MIT", | ||
}, | ||
"main": "src/browser/nuemark.js", | ||
"dependencies": { | ||
@@ -15,0 +18,0 @@ "js-yaml": "^4.1.0", |
@@ -6,2 +6,58 @@ | ||
# Work in progress | ||
# Nuekit | ||
Nuemark is a Markdown dialect for rich, interactive content. Use it to create marketing pages, documentation pages, and blog entries without leaving the content. Even you all-mighty front page can be expressed with Nuemark: | ||
![Web page in read and edit mode](https://nuejs.org/img/nuemark-content-big.png) | ||
*In above*: The content and the result. Nuemark puts you into content-first mindset. | ||
### Documentation | ||
[What is Nuemark?](https://nuejs.org/blog/introducing-nuemark/) | ||
[User guide](https://nuejs.org/docs/concepts/nuemark.html) | ||
[Tag reference](https://nuejs.org/docs/reference/nuemark-tags.html) | ||
[API docs](https://nuejs.org/docs/reference/nuemark-api.html) | ||
### Getting Started | ||
``` | ||
# Install Bun (if not done yet) | ||
curl -fsSL https://bun.sh/install | bash | ||
# Install website generator. This includes Nuemark | ||
bun install nuekit --global | ||
# Install Nuemark demo (as seen on this page) | ||
bun create nue@latest | ||
``` | ||
Choose “nuemark-demo” to see the above big screenshot in action. | ||
Refer to [getting started docs](https://nuejs.org/docs/#node) if you prefer Node. | ||
### The big picture | ||
The ultimate goal of Nue is to build a content first alternative to **Vercel** and **Netlify**, which is extremely fast and ridiculously easy to use. | ||
![Nue Roadmap](https://nuejs.org/img/roadmap4-big.png) | ||
#### Why Nue? | ||
- [Content first](https://nuejs.org/docs/why-nue/content-first.html) | ||
- [Extreme performance](https://nuejs.org/docs/why-nue/extreme-performance.html) | ||
- [Closer to standards](https://nuejs.org/docs/why-nue/closer-to-standards.html) | ||
### Contributing | ||
Please see [contributing.md](/CONTRIBUTING.md) | ||
### Community | ||
Please see [GitHub discussions](https://github.com/nuejs/nue/discussions) | ||
@@ -10,19 +10,5 @@ | ||
const { str, getValue } = valueGetter(input) | ||
let [name, ...attribs] = str.split(/\s+/) | ||
const self = { attr: {}, data: {} } | ||
const [specs, ...attribs] = str.split(/\s+/) | ||
const self = { ...parseSpecs(specs), data: {} } | ||
// #id or .class | ||
const i = name.search(/[\#\.]/) | ||
if (!i) { | ||
attribs.unshift(name) | ||
name = undefined | ||
// <name>.class | ||
} else if (i > 0) { | ||
attribs.unshift(name.slice(i)) | ||
name = name.slice(0, i) | ||
} | ||
for (const el of attribs) { | ||
@@ -38,15 +24,8 @@ const [key, val] = el.split('=') | ||
} else { | ||
// #id.foo.bar | ||
if ('.#'.includes(key[0])) { | ||
Object.assign(self.attr, parseAttr(key)) | ||
} else { | ||
if (self.data._) self.data[key] = true | ||
else self.data._ = getValue(key) || key | ||
} | ||
if (self.data._) self.data[key] = true | ||
else self.data._ = getValue(key) || key | ||
} | ||
} | ||
return { ...self, name } | ||
return self | ||
} | ||
@@ -76,12 +55,27 @@ | ||
// .foo#bar.baz -> class: ['foo', 'bar'], id: 'bar' | ||
// tabs.foo#bar.baz -> { name: 'tabs', class: ['foo', 'bar'], id: 'bar' } | ||
export function parseSpecs(str) { | ||
const self = { name: str, attr: {} } | ||
const i = str.search(/[\#\.]/) | ||
if (i >= 0) { | ||
self.name = str.slice(0, i) || null | ||
self.attr = parseAttr(str.slice(i)) | ||
} | ||
return self | ||
} | ||
export function parseAttr(str) { | ||
const attr = {} | ||
// classes | ||
const classes = [] | ||
const ret = {} | ||
str.replace(/\.([\w\-]+)/g, (_, el) => classes.push(el)) | ||
str.replace(/#([\w\-]+)/, (_, el) => ret.id = el) | ||
if (classes[0]) ret.class = classes.join(' ') | ||
if (classes[0]) attr.class = classes.join(' ') | ||
return Object.keys(ret)[0] && ret | ||
// id | ||
str.replace(/#([\w\-]+)/, (_, el) => attr.id = el) | ||
return attr | ||
} | ||
@@ -88,0 +82,0 @@ |
140
src/parse.js
import { parseAttr, parseSpecs, parseComponent } from './component.js' | ||
import { loadAll, load as parseYAML } from 'js-yaml' | ||
import { parseComponent } from './component.js' | ||
import { ISOMORPHIC } from './tags.js' | ||
import { marked } from 'marked' | ||
const NL = '\n' | ||
// returns { meta, sections, headings, links } | ||
export function parsePage(lines) { | ||
if (typeof lines == 'string') lines = lines.split(NL) | ||
// { meta, sections, headings, links } | ||
export function parsePage(lines) { | ||
const sections = [], headings = [], links = {} | ||
const { meta, rest } = parseMeta(lines) | ||
for (const block of parseBlocks(rest)) { | ||
const { name, data, body } = block | ||
const sections = parseSections(rest) | ||
const headings = [], links = {} | ||
let isomorphic | ||
if (body) { | ||
const content = body.join('\n') | ||
if (name) Object.assign(data, getNestedData(content)) | ||
else data.content = content.split('---') | ||
delete block.body | ||
} | ||
for (const section of sections) { | ||
const blocks = section.blocks = [] | ||
// component | ||
if (data) { | ||
sections.push(block) | ||
for (const block of parseBlocks(section.lines)) { | ||
const { name, data, body } = block | ||
// markdown | ||
} else { | ||
const tokens = marked.lexer(block.join('\n')) | ||
Object.assign(links, tokens.links) | ||
headings.push(...tokens.filter(el => el.type == 'heading').map(parseHeading)) | ||
sections.push({ md: block, tokens }) | ||
if (name && ISOMORPHIC.includes(name)) isomorphic = true | ||
// component body | ||
if (body) { | ||
const content = body.join(NL) | ||
if (name) Object.assign(data, getNestedData(content)) | ||
else data.content = content.split('---') | ||
delete block.body | ||
} | ||
// component or fenced code block | ||
if (data || block.code) { | ||
blocks.push(block) | ||
// markdown | ||
} else { | ||
const tokens = marked.lexer(block.join(NL)) | ||
Object.assign(links, tokens.links) | ||
headings.push(...tokens.filter(el => el.type == 'heading').map(el => { | ||
return { level: el.depth, ...parseHeading(el.text) } | ||
})) | ||
blocks.push({ md: block, tokens }) | ||
} | ||
} | ||
@@ -40,17 +57,19 @@ } | ||
return { meta, sections, headings, links } | ||
return { meta, sections, headings, links, isomorphic } | ||
} | ||
export function parseHeading({ depth, text, id }) { | ||
const i = text.indexOf(' {#') | ||
export function parseHeading(text) { | ||
const i = text.indexOf('{') | ||
if (i > 0 && text.endsWith('}')) { | ||
id = text.slice(i + 3, -1) | ||
text = text.slice(0, i) | ||
const attr = parseAttr(text.slice(i+1, -1).trim()) | ||
return { text: text.slice(0, i).trim(), ...attr } | ||
} | ||
return { level: depth, text, id: id || createHeaderId(text) } | ||
return { text, id: createHeaderId(text) } | ||
} | ||
export function createHeaderId(text) { | ||
let hash = text.replace(/\W/g, '-').replace(/-+/g, '-').toLowerCase() | ||
let hash = text.replace(/'/g, '').replace(/\W/g, '-').replace(/-+/g, '-').toLowerCase() | ||
if (hash[0] == '-') hash = hash.slice(1) | ||
@@ -63,3 +82,2 @@ if (hash.endsWith('-')) hash = hash.slice(0, -1) | ||
export function parseMeta(lines) { | ||
const isFront = (line) => line == '---' | ||
var start = 0, end = -1 | ||
@@ -69,10 +87,14 @@ | ||
const line = lines[i] | ||
if (!start) { if (isFront(line)) start = i + 1 } | ||
else if (isFront(line)) { end = i; break } | ||
const is_front = line == '---' | ||
if (!start) { | ||
if (is_front) start = i + 1 | ||
else if (line.trim()) return { rest: lines, meta: {} } | ||
} | ||
else if (is_front) { end = i; break } | ||
} | ||
const front = start ? lines.slice(start, end).join('\n') : '' | ||
const front = start ? lines.slice(start, end).join(NL) : '' | ||
return { | ||
meta: front ? parseYAML(front) : {}, | ||
meta: front ? parseYAML(front) || {} : {}, | ||
rest: lines.slice(end + 1) | ||
@@ -99,5 +121,30 @@ } | ||
export function parseSections(lines) { | ||
const len = lines.length | ||
const sections = [] | ||
let section = [] | ||
function push(attr) { | ||
sections.push({ lines: section, attr }) | ||
} | ||
push() | ||
lines.forEach(line => { | ||
if (line.startsWith('---')) { | ||
section = [] // must be before push | ||
const i = line.indexOf('- ') | ||
push(i > 0 ? parseAttr(line.slice(i + 2).trim()) : null) | ||
} else { | ||
section.push(line) | ||
} | ||
}) | ||
return sections | ||
} | ||
export function parseBlocks(lines) { | ||
const blocks = [] | ||
let md, comp | ||
let md, fenced, comp | ||
let spacing | ||
@@ -107,8 +154,17 @@ | ||
// fenced code start | ||
const ltrim = line.trimStart() | ||
// fenced code start/end | ||
if (line.startsWith('```')) { | ||
if (!fenced) { | ||
fenced = { code: [], ...parseSpecs(line.slice(3).trim()) } | ||
} else { | ||
blocks.push(fenced) | ||
fenced = null | ||
} | ||
return | ||
} | ||
// comment | ||
if (ltrim.startsWith('//# ')) return | ||
// code line | ||
if (fenced) return fenced.code.push(line) | ||
// component | ||
@@ -131,3 +187,3 @@ if (line[0] == '[' && line.slice(-1) == ']' && !line.includes('][')) { | ||
if (!ltrim) comp.body?.push(line) | ||
if (!line.trimStart()) comp.body?.push(line) | ||
else if (!getIndent(line)) comp = null | ||
@@ -138,5 +194,7 @@ } | ||
if (!comp) { | ||
if (line.trimStart().startsWith('//')) return | ||
if (!md) blocks.push(md = []) | ||
md.push(line) | ||
} | ||
}) | ||
@@ -148,4 +206,4 @@ | ||
function getIndent(line='') { | ||
const ltrim = line.trimStart() | ||
return line.length - ltrim.length | ||
const trim = line.trimStart() | ||
return line.length - trim.length | ||
} | ||
@@ -152,0 +210,0 @@ |
import { tags, elem, join, concat } from './tags.js' | ||
import { parsePage, parseHeading } from './parse.js' | ||
import { tags, elem } from './tags.js' | ||
import { parseAttr } from './component.js' | ||
import { marked } from 'marked' | ||
@@ -8,35 +9,51 @@ | ||
export function renderPage(page, opts) { | ||
const { data={}, lib=[], highlight } = opts | ||
const { lib=[] } = opts | ||
const data = { ...opts.data, ...page.meta } | ||
const draw_sections = data?.draw_sections || page.sections[1] | ||
const section_attr = data.sections || [] | ||
const ret = [] | ||
// syntax highlighter | ||
marked.setOptions({ highlight }) | ||
const html = page.sections.map(el => { | ||
const { name, md, attr } = el | ||
// section_attr | ||
page.sections.forEach((section, i) => { | ||
const comp = name && lib.find(el => el.name == name) | ||
const alldata = { ...data, ...el.data, attr } | ||
const tag = tags[name] | ||
const html = join(section.blocks.map(el => { | ||
const { name, md, attr } = el | ||
const comp = name && lib.find(el => [name, toCamelCase(name)].includes(el.name)) | ||
const alldata = { ...data, ...el.data, attr } | ||
const tag = tags[name] | ||
// tag | ||
return tag ? tag(alldata, opts) : | ||
// tag | ||
return tag ? tag(alldata, opts) : | ||
// server component | ||
comp ? comp.render(alldata, lib) : | ||
// component | ||
comp ? comp.render(alldata, lib) : | ||
// markdown | ||
md ? renderMD(md, mergeLinks(page.links, data.links)) : | ||
// fenced code | ||
el.code ? renderCodeBlock(el, opts.highlight) : | ||
// island | ||
name ? renderIsland(el) : | ||
// markdown | ||
md ? renderMarkdown(md, mergeLinks(page.links, data.links)) : | ||
// section | ||
tags.section(alldata, opts) | ||
// island | ||
name ? renderIsland(el) : | ||
}).join('\n') | ||
// generic layout | ||
tags.layout(alldata, opts) | ||
return { ...page, html } | ||
})) | ||
const attr = section.attr || parseAttr(section_attr[i] || '') | ||
ret.push(draw_sections ? elem('section', attr, html) : html) | ||
}) | ||
return { ...page, html: join(ret) } | ||
} | ||
export function render(lines, opts={}) { | ||
function toCamelCase(str) { | ||
return str.split('-').map(el => el[0].toUpperCase() + el.slice(1)).join('') | ||
} | ||
export function renderLines(lines, opts={}) { | ||
const page = parsePage(lines) | ||
@@ -46,2 +63,10 @@ return renderPage(page, opts) | ||
export function renderCodeBlock({ name, code, attr }, fn) { | ||
// console.info(name, code, attr, fn) | ||
if (name) attr.class = concat(`syntax-${name}`, attr.class) | ||
const body = join(code) | ||
return elem('pre', attr, fn ? fn(body) : body) | ||
} | ||
// export function renderPage() | ||
@@ -58,8 +83,10 @@ | ||
/* | ||
Marked does not support editing of the AST abstract syntax tree regarding links | ||
So we have no use of the already tokenized tree and have to re-parse here :( | ||
Sadly, Marked does not have a modifiable abstract syntax tree (AST) so | ||
internally we must render all markdown blocks twice: | ||
https://github.com/markedjs/marked/issues/3135 | ||
Looking for other Markdown implementations if this becomes a performance bottleneck. | ||
*/ | ||
export function renderMD(md, links) { | ||
export function renderMarkdown(md, links) { | ||
md.push('') | ||
@@ -71,3 +98,3 @@ | ||
} | ||
return marked.parse(md.join('\n')) | ||
return marked.parse(join(md)) | ||
} | ||
@@ -99,10 +126,10 @@ | ||
heading(html, level, raw) { | ||
const plain = parseHeading({ text: raw }) | ||
const rich = parseHeading({ text: html }) | ||
const plain = parseHeading(raw) | ||
const cls = plain.class | ||
const title = plain.text.replaceAll('"', '') | ||
const rich = parseHeading(html) | ||
return [ | ||
`<h${level} id="${plain.id}">${rich.text}`, | ||
`<a href="#${plain.id}" title='Permlink for "${plain.text}"'></a>`, | ||
`</h${level}>\n` | ||
].join('') | ||
delete plain.text | ||
const a = elem('a', { href: `#${plain.id}`, title: `Permlink for '${title}'` }) | ||
return elem(`h${level}`, plain, a + rich.text) | ||
}, | ||
@@ -109,0 +136,0 @@ |
@@ -24,5 +24,8 @@ | ||
import { renderCodeBlock } from './render.js' | ||
import { nuemarkdown } from '../index.js' | ||
import { parseInline } from 'marked' | ||
import { nuemarkdown } from '..' | ||
// list all tags that require a client-side Web Component | ||
export const ISOMORPHIC = ['tabs'] | ||
@@ -43,8 +46,8 @@ export const tags = { | ||
table({ attr, head, items=[] }) { | ||
const ths = toArray(head).map(val => elem('th', val)) | ||
table({ attr, head, _, items=[] }) { | ||
const ths = toArray(head || _).map(val => elem('th', parseInline(val.trim()))) | ||
const thead = elem('thead', elem('tr', join(ths))) | ||
const trs = items.map(row => { | ||
const tds = toArray(row).map(val => elem('td', val)) | ||
const tds = toArray(row).map(val => elem('td', parseInline(val.trim()))) | ||
return elem('tr', join(tds)) | ||
@@ -56,15 +59,4 @@ }) | ||
grid(data, opts) { | ||
const { attr, content=[]} = data | ||
const extra = { style: '--cols: 1fr 1fr 1fr', class: concat('grid', attr.class) } | ||
const divs = content.map((str, i) => { | ||
const attr = i + 1 == content.length ? { style: '--colspan: 3' } : {} | ||
return elem('div', attr, nuemarkdown(str, opts)) | ||
}) | ||
return elem('section', { ...attr, ...extra }, join(divs)) | ||
}, | ||
section(data, opts) { | ||
// generic layout block | ||
layout(data, opts) { | ||
const { content=[]} = data | ||
@@ -78,3 +70,3 @@ // const bc = data.block_class || 'block' | ||
return elem('section', data.attr, join(divs)) | ||
return elem(divs[1] ? 'section' : 'div', data.attr, join(divs)) | ||
}, | ||
@@ -123,3 +115,3 @@ | ||
// inneer <source> tags | ||
// inner <source> tags | ||
const html = sources.map(src => elem('source', { src, type: getMimeType(src) }) ) | ||
@@ -140,3 +132,3 @@ | ||
tabs(data, opts) { | ||
const { attr, name='tab', content=[], _ } = data | ||
const { attr, key='tab', content=[], _ } = data | ||
const half = Math.round(content.length / 2) | ||
@@ -148,3 +140,3 @@ const t = _ || data.tabs | ||
const html = t ? el : nuemarkdown(el, opts) | ||
return elem('a', { href: `#${name}-${i+1}` }, html ) | ||
return elem('a', { href: `#${key}-${i+1}` }, html ) | ||
}) | ||
@@ -154,6 +146,6 @@ | ||
const html = nuemarkdown(el, opts) | ||
return elem('li', { id: `${name}-${i+1}` }, html ) | ||
return elem('li', { id: `${key}-${i+1}` }, html ) | ||
}) | ||
return elem('section', { is: 'nue-tabs', class: 'tabs', ...attr }, | ||
return elem('section', { is: 'nuemark-tabs', class: 'tabs', ...attr }, | ||
elem('nav', join(tabs)) + | ||
@@ -164,2 +156,5 @@ elem('ul', join(panes)) | ||
/* later | ||
codetabs(data, opts) { | ||
@@ -174,4 +169,4 @@ const { content=[] } = data | ||
content.forEach((code, i) => { | ||
const type = types[i] || data.type || '' | ||
content[i] = join([('``` ' + type).trim(), code, '```']) | ||
const name = types[i] || data.type || '' | ||
content[i] = renderCodeBlock({ name, code, attr: {} }, opts.highlight) | ||
}) | ||
@@ -182,2 +177,16 @@ | ||
grid(data, opts) { | ||
const { attr, content=[], _='a'} = data | ||
const { cols, colspan } = getGridCols(content.length, _) | ||
const extra = { style: `--cols: ${cols}`, class: concat('grid', attr.class) } | ||
const divs = content.map((str, i) => { | ||
const attr = colspan && i + 1 == content.length ? { style: `--colspan: ${colspan}` } : {} | ||
return elem('div', attr, nuemarkdown(str, opts)) | ||
}) | ||
return elem('section', { ...attr, ...extra }, join(divs)) | ||
}, | ||
*/ | ||
} | ||
@@ -221,3 +230,3 @@ | ||
function join(els, separ='\n') { | ||
export function join(els, separ='\n') { | ||
return els?.join ? els.join(separ) : els | ||
@@ -227,3 +236,3 @@ } | ||
// concat two strings (for class attribute) | ||
function concat(a, b) { | ||
export function concat(a, b) { | ||
return join([a || '', b || ''], ' ').trim() | ||
@@ -233,7 +242,7 @@ } | ||
export function createPicture(img_attr, data) { | ||
const { small, offset=768 } = data | ||
const { small, offset=750 } = data | ||
const sources = [small, img_attr.src].map(src => { | ||
const prefix = src == small ? 'max' : 'min' | ||
const media = `(${prefix}-width: ${offset}px)` | ||
const media = `(${prefix}-width: ${parseInt(offset)}px)` | ||
return elem('source', { src, media, type: getMimeType(src) }) | ||
@@ -247,2 +256,17 @@ }) | ||
/* more complex grids later | ||
const GRID = { | ||
a: [2, 3, 2, '2/2', 3, '3/3', 4, 3], | ||
b: [2, '2/2', '3/3'] | ||
} | ||
export function getGridCols(am, variant='a') { | ||
const val = GRID[variant][am -2] | ||
if (!val) return { cols: '1fr 1fr' } | ||
const [count, span] = val.toString().split('/') | ||
return { cols: Array(1 * count).fill('1fr').join(' '), colspan: 1 * span } | ||
} | ||
*/ | ||
function getVideoAttrs(data) { | ||
@@ -249,0 +273,0 @@ const keys = 'autoplay controls loop muted poster preload src width'.split(' ') |
import { nuemarkdown } from '../index.js' | ||
import { promises as fs } from 'node:fs' | ||
import { nuemarkdown } from '..' | ||
@@ -5,0 +5,0 @@ const opts = {} |
import { parseComponent, valueGetter, parseAttr } from '../src/component.js' | ||
import { parseMeta, parseBlocks, parsePage } from '../src/parse.js' | ||
import { renderIsland, render } from '../src/render.js' | ||
import { parseMeta, parseBlocks, parseSections, parsePage, parseHeading } from '../src/parse.js' | ||
import { parseComponent, valueGetter, parseAttr, parseSpecs } from '../src/component.js' | ||
import { renderIsland, renderLines } from '../src/render.js' | ||
import { nuemarkdown } from '../index.js' | ||
import { tags } from '../src/tags.js' | ||
test('nested code with comment', () => { | ||
const { html } = renderLines(['[.hey]', ' // not rendered', ' ```', ' // here', ' ```']) | ||
expect(html).toBe('<div class="hey"><pre>// here</pre></div>') | ||
}) | ||
test('render fenced code', () => { | ||
const { html } = renderLines(['``` md.foo#bar', '// hey', '```']) | ||
expect(html).toBe('<pre class="syntax-md foo" id="bar">// hey</pre>') | ||
}) | ||
test('parse fenced code', () => { | ||
const blocks = parseBlocks(['# Hey', '``` md.foo#bar', '// hey', '[foo]', '```']) | ||
const [ hey, fenced ] = blocks | ||
expect(fenced.name).toBe('md') | ||
expect(fenced.attr).toEqual({ class: 'foo', id: 'bar' }) | ||
expect(fenced.code).toEqual([ "// hey", "[foo]" ]) | ||
}) | ||
test('[!] img', () => { | ||
const icon = tags['!']({ _: 'cat' }) | ||
expect(icon).toStartWith('<img src="/img/cat.svg"') | ||
const img = tags['!']({ _: 'img.png' }) | ||
@@ -28,4 +52,4 @@ expect(img).toStartWith('<img src="img.png"') | ||
test('[tabs] attr', () => { | ||
const html = tags.tabs({ _: 't1, t2', name: 'hey', content: ['c1', 'c2'] }) | ||
expect(html).toInclude('<section is="nue-tabs" class="tabs">') | ||
const html = tags.tabs({ _: 't1, t2', key: 'hey', content: ['c1', 'c2'] }) | ||
expect(html).toInclude('<section is="nuemark-tabs" class="tabs">') | ||
expect(html).toInclude('<nav><a href="#hey-1">t1</a>') | ||
@@ -37,3 +61,3 @@ expect(html).toInclude('<li id="hey-2"><p>c2</p>') | ||
const html = tags.tabs({ content: 'abcd'.split(''), attr: { id: 'hey' } }) | ||
expect(html).toInclude('<section is="nue-tabs" class="tabs" id="hey">') | ||
expect(html).toInclude('<section is="nuemark-tabs" class="tabs" id="hey">') | ||
expect(html).toInclude('<nav><a href="#tab-1"><p>a</p>') | ||
@@ -86,20 +110,19 @@ expect(html).toInclude('<li id="tab-2"><p>d</p>') | ||
test('[section]', () => { | ||
test('[layout]', () => { | ||
const attr = { id: 'epic' } | ||
const data = { count: 10 } | ||
const single = tags.section({ attr, data, content: ['foo'] }) | ||
const single = tags.layout({ attr, data, content: ['foo'] }) | ||
expect(single).toInclude('<section id="epic">') | ||
expect(single).toInclude('<div id="epic">') | ||
expect(single).toInclude('<p>foo</p>') | ||
const double = tags.section({ attr, data, content: ['foo', 'bar'] }) | ||
const double = tags.layout({ attr, data, content: ['foo', 'bar'] }) | ||
expect(double).toInclude('<section id="epic">') | ||
expect(double).toInclude('block block-2') | ||
}) | ||
test('[section] with nested component', () => { | ||
test('[layout] with nested component', () => { | ||
const content = ['# Hello', '## World\n[image "joo.png"]'] | ||
const html = tags.section({ content }) | ||
const html = tags.layout({ content }) | ||
expect(html).toInclude('<h1 id="hello">') | ||
expect(html).toInclude('block block-2') | ||
expect(html).toInclude('<div><h2 id="world">') | ||
expect(html).toInclude('img src="joo.png"') | ||
@@ -124,14 +147,20 @@ }) | ||
// page rendering | ||
test('render sections', () => { | ||
const lines = ['a', 'a', '--- #a.b', 'b', 'b', '---', 'c', 'c'] | ||
const { html } = renderLines(lines, { data: { sections: ['#foo']}}) | ||
expect(html).toStartWith('<section id="foo"><p>a') | ||
expect(html).toInclude('<section class="b" id="a"><p>b') | ||
expect(html).toInclude('<section><p>c') | ||
}) | ||
test('generic section', () => { | ||
const { html } = render(['[.info]', ' # Hello', ' para', ' ---', ' World']) | ||
expect(html).toInclude('block block-2') | ||
const { html } = renderLines(['[.info]', ' # Hello', ' para', ' ---', ' World']) | ||
expect(html).toInclude('<section class="info">') | ||
expect(html).toInclude('<p>para</p>') | ||
}) | ||
test('reflinks', () => { | ||
const links = { dude: '//hey.net "boom"' } | ||
const lines = ['[hey][yolo] [dude][dude]', '[.foo]', '[yolo]: yolo.co "lol"'] | ||
const { html } = render(lines, { data: { links } }) | ||
const { html } = renderLines(lines, { data: { links } }) | ||
@@ -142,11 +171,22 @@ expect(html).toInclude('<a href="yolo.co" title="lol">hey</a>') | ||
test('header id', () => { | ||
const { html } = render(['# Hey baari on jotain {#custom}']) | ||
expect(html).toInclude('<h1 id="custom">') | ||
expect(html).toInclude('<a href="#custom"') | ||
test('parseHeading', () => { | ||
const h1 = parseHeading('# Hey { #me-too.hey.yo }') | ||
expect(h1.text).toBe('# Hey') | ||
expect(h1.id).toBe('me-too') | ||
expect(h1.class).toBe('hey yo') | ||
const h2 = parseHeading('## Hey') | ||
expect(h2.text).toBe('## Hey') | ||
expect(h2.id).toBe('hey') | ||
}) | ||
test('heading id', () => { | ||
const { html } = renderLines(['# Hey _boy_ { #me.too }']) | ||
expect(html).toInclude('<h1 class="too" id="me">') | ||
expect(html).toInclude('<a href="#me"') | ||
expect(html).toInclude('Hey <em>boy</em>') | ||
}) | ||
test('page island', () => { | ||
const { html } = render(['yo', '[hey]', ' bar: 2']) | ||
const { html } = renderLines(['yo', '[hey]', ' bar: 2']) | ||
expect(html).toInclude('<p>yo</p>') | ||
@@ -157,10 +197,28 @@ expect(html).toInclude('nue-island island="hey"') | ||
// rendering blocks | ||
test('renderIsland', () => { | ||
const attr = { id: 'epic' } | ||
const data = { count: 10 } | ||
const island = renderIsland({ name: 'foo', attr, data }) | ||
expect(island).toInclude('id="epic" island="foo"') | ||
expect(island).toInclude('{"count":10}') | ||
}) | ||
// page parsing | ||
test('parse sections', () => { | ||
const els = parseSections(['a', 'a']) | ||
expect(els[0].lines).toEqual(['a', 'a']) | ||
expect(els.length).toBe(1) | ||
}) | ||
test('[!] parse', () => { | ||
const page = parsePage(['[! "/foo"]']) | ||
const { data, name } = page.sections[0] | ||
expect(data._).toBe('/foo') | ||
expect(name).toBe('!') | ||
test('parse sections', () => { | ||
const lines = ['a', 'a', '--- #a.b', 'b', 'b', '---', 'c', 'c'] | ||
const sections = parseSections(lines) | ||
const [a, b, c] = sections | ||
expect(sections.length).toBe(3) | ||
expect(a.lines).toEqual(['a', 'a']) | ||
expect(b.attr).toEqual({ id: "a", class: "b" }) | ||
expect(c.lines).toEqual(['c', 'c']) | ||
}) | ||
@@ -170,11 +228,22 @@ | ||
test('parse page', () => { | ||
const page = parsePage(['# Hello', '## World', '[foo]: bar', '[hey foo=1]', ' bar: 2']) | ||
const { sections } = page | ||
const page = parsePage(['# Hello', '## World', '[foo]: bar', '[tabs foo=1]', ' bar: 2']) | ||
expect(page.isomorphic).toBe(true) | ||
expect(page.links).toHaveProperty('foo') | ||
expect(page.headings.length).toBe(2) | ||
expect(sections.length).toBe(2) | ||
expect(sections[1].data).toEqual({ foo: 1, bar: 2 }) | ||
const { blocks } = page.sections[0] | ||
expect(blocks.length).toBe(2) | ||
expect(blocks[1].data).toEqual({ foo: 1, bar: 2 }) | ||
}) | ||
test('parse page: ! component', () => { | ||
const page = parsePage(['[! "/foo"]']) | ||
const { data, name } = page.sections[0].blocks[0] | ||
expect(data._).toBe('/foo') | ||
expect(name).toBe('!') | ||
}) | ||
// blocks within sections | ||
test('parse blocks', () => { | ||
@@ -210,17 +279,3 @@ const blocks = parseBlocks(['Hello', '[foo]', ' bar: 10', 'World']) | ||
// rendering blocks | ||
test('renderIsland', () => { | ||
const attr = { id: 'epic' } | ||
const data = { count: 10 } | ||
const island = renderIsland({ name: 'foo', attr, data }) | ||
expect(island).toInclude('id="epic" island="foo"') | ||
expect(island).toInclude('{"count":10}') | ||
}) | ||
// parsing components | ||
test('valueGetter', () => { | ||
@@ -234,3 +289,2 @@ const { str, getValue } = valueGetter(`foo="yo" bar="hey dude"`) | ||
test('parseAttr', () => { | ||
expect(parseAttr('#foo.bar')).toEqual({ id: 'foo', class: 'bar'}) | ||
expect(parseAttr('.bar#foo')).toEqual({ id: 'foo', class: 'bar'}) | ||
@@ -240,2 +294,6 @@ expect(parseAttr('.bar#foo.baz')).toEqual({ id: 'foo', class: 'bar baz'}) | ||
test('parseSpecs', () => { | ||
expect(parseSpecs('tabs')).toEqual({ name: 'tabs', attr: {} }) | ||
expect(parseSpecs('tabs.#foo.bar')).toEqual({ name: 'tabs', attr: { id: 'foo', class: 'bar' }}) | ||
}) | ||
@@ -245,3 +303,3 @@ test('parseComponent', () => { | ||
expect(parseComponent('#foo.bar')).toEqual({ | ||
attr: { id: "foo", class: "bar" }, data: {}, | ||
name: null, attr: { id: "foo", class: "bar" }, data: {}, | ||
}) | ||
@@ -265,3 +323,3 @@ | ||
expect(parseComponent('info "Sure ??" #alert class="boss"')).toEqual({ | ||
expect(parseComponent('info#alert "Sure ??" class="boss"')).toEqual({ | ||
name: 'info', attr: { class:'boss', id: 'alert' }, data: { _: 'Sure ??' }, | ||
@@ -272,5 +330,54 @@ }) | ||
/* | ||
Required: | ||
test('syntax highlight', async () => { | ||
bun add react | ||
bun add react-dom | ||
*/ | ||
test('JSX component', async () => { | ||
try { | ||
// import React SSR (server side rendering) API method | ||
const { renderToString } = await import('react-dom/server') | ||
// import custom JSX components | ||
const jsx = await import('./react-lib') | ||
// make them compatible with Nuemark | ||
const lib = Object.keys(jsx).map(name => { | ||
return { name, render: (data) => renderToString(jsx[name](data)) } | ||
}) | ||
// render JSX with Nuemark | ||
const html = nuemarkdown('[my-test]', { lib, data: { message: 'Hello' } }) | ||
expect(html).toBe('<h1 style="color:red">Hello</h1>') | ||
// react not imported | ||
} catch (ignored) { | ||
console.info('JSX test skipped') | ||
} | ||
}) | ||
// The following tags are released later | ||
test.skip('[grid]', () => { | ||
const html = tags.grid({ content: 'abcdefg'.split(''), attr: { class: 'foo' } }) | ||
expect(html).toInclude('<section class="grid foo" style="--cols: 1fr 1fr 1fr"') | ||
expect(html).toInclude('<div style="--colspan: 3"><p>g</p>') | ||
}) | ||
test.skip('grid columns', () => { | ||
const grid = getGridCols(5, 'a') | ||
expect(grid.cols).toBe('1fr 1fr') | ||
expect(grid.colspan).toBe(2) | ||
}) | ||
test.skip('nue color', async () => { | ||
try { | ||
const nuecolor = await import('nuecolor') | ||
@@ -280,14 +387,19 @@ const opts = { highlight: nuecolor.default } | ||
// syntax block | ||
const { html } = render(['``` md', '# hey', '```'], opts) | ||
expect(html).toInclude('<pre><code class="language-md">') | ||
expect(html).toInclude('<b class=hl-heading> hey</b>') | ||
const { html } = renderLines(['``` md.foo', '# hey', '```'], opts) | ||
expect(html).toInclude('<pre class="syntax-md foo">') | ||
expect(html).toInclude('<b class=hl-char>#</b> hey<') | ||
// code tabs | ||
const tabs = tags.codetabs({ _: 't1, t2', content: ['# c1', '*c2*'], type: 'md' }, opts) | ||
expect(tabs).toInclude('<a href="#tab-1">t1</a>') | ||
expect(tabs).toInclude('<b class=hl-heading> c1</b>') | ||
expect(tabs).toInclude('</code></pre>') | ||
expect(tabs).toInclude('<b class=hl-char>*</b>') | ||
expect(tabs).toInclude('</pre>') | ||
} catch(ignore) { console.info(ignore) /* highlighter not found */ } | ||
// highlighter not found | ||
} catch(ignore) { | ||
console.info('nuecolor not found') | ||
} | ||
}) | ||
}) |
--- | ||
# draw_sections: true | ||
# sections: [.a#b, .c#d] | ||
--- | ||
# Hey | ||
[codetabs tabs="yo | boy"] | ||
hey | ||
--- | ||
joe | ||
[.some#dorg] | ||
kamaa | ||
Hello |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
33657
12
898
63