poops 1.9.7 → 1.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1023,6 +1023,20 @@ The language argument is optional. If omitted, highlight.js will attempt to auto
1023
1023
 
1024
1024
  Registered languages: `javascript`/`js`, `typescript`/`ts`, `css`, `scss`, `html`, `xml`, `json`, `bash`/`sh`, `shell`, `python`/`py`, `ruby`/`rb`, `php`, `java`, `c`, `cpp`, `csharp`/`cs`, `go`, `rust`/`rs`, `yaml`/`yml`, `markdown`/`md`, `sql`, `diff`.
1025
1025
 
1026
+ **Fence info strings.** Only the first word names the language. Anything after it is carried onto the `<code>` element instead of being dropped: a bare word becomes a class, a `key=value` token becomes a `data-` attribute. This is how a fence marks itself for a later stage — a post-`markup` `exec` script that turns `code.preview` blocks into live demos, for example — without a marker comment in the markdown.
1027
+
1028
+ ````md
1029
+ ```html preview tab=options widths=375,768
1030
+ <my-element></my-element>
1031
+ ```
1032
+ ````
1033
+
1034
+ ```html
1035
+ <pre><code class="hljs language-html preview" data-tab="options" data-widths="375,768">…</code></pre>
1036
+ ```
1037
+
1038
+ Values are single tokens — no quotes, no spaces. A trailing `=` with nothing after it emits a valueless attribute (`expanded=` → `data-expanded=""`), for a flag you want to read with `hasAttribute` rather than as a class. The same applies to the `{% highlight %}` tag and the `highlight` filter.
1039
+
1026
1040
  #### Custom Filters
1027
1041
 
1028
1042
  All filters are available in both engines. The only syntax difference is how arguments are passed: Nunjucks uses parentheses `| filter("arg")`, Liquid uses a colon `| filter: "arg"`.
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { Liquid } from 'liquidjs'
4
- import { highlightCode } from '../highlight.js'
4
+ import { highlightCode, codeBlock } from '../highlight.js'
5
5
  import { marked, renderMarkdownCached } from '../renderer.js'
6
6
  import { discoverImageVariants, parseFrontMatter, groupby, buildImageTag, buildPaginationTag, renderToc, buildJsonLd, buildOpenGraph, buildCanonical, buildBreadcrumb } from '../helpers.js'
7
7
  import { getImageExif, listImages } from '../image-cache.js'
@@ -131,11 +131,7 @@ export default class LiquidEngine {
131
131
  engine.registerFilter('canonical', (page, site) => buildCanonical(page, site))
132
132
  engine.registerFilter('breadcrumb', (page, site, prefix) => buildBreadcrumb(page, site, prefix))
133
133
  engine.registerFilter('groupby', (arr, key, datePart) => groupby(arr, key, datePart))
134
- engine.registerFilter('highlight', (code, lang) => {
135
- const highlighted = highlightCode(code, lang)
136
- const langClass = lang ? ` language-${lang}` : ''
137
- return `<pre><code class="hljs${langClass}">${highlighted}</code></pre>`
138
- })
134
+ engine.registerFilter('highlight', (code, lang) => codeBlock(highlightCode(code, lang), lang))
139
135
  }
140
136
 
141
137
  registerTags(getOutputDir) {
@@ -347,10 +343,7 @@ function registerHighlightTag(engine) {
347
343
  },
348
344
  * render(ctx) {
349
345
  const code = yield this.liquid.renderer.renderTemplates(this.templates, ctx)
350
- const lang = this.lang
351
- const highlighted = highlightCode(code, lang)
352
- const langClass = lang ? ` language-${lang}` : ''
353
- return `<pre><code class="hljs${langClass}">${highlighted}</code></pre>`
346
+ return codeBlock(highlightCode(code, this.lang), this.lang)
354
347
  }
355
348
  })
356
349
  }
@@ -6,7 +6,7 @@ import path from 'node:path'
6
6
  import { discoverImageVariants, parseFrontMatter, groupby, buildImageTag, buildPaginationTag, renderToc, buildJsonLd, buildOpenGraph, buildCanonical, buildBreadcrumb } from '../helpers.js'
7
7
  import { getImageExif, listImages } from '../image-cache.js'
8
8
  import { toPosix } from '../../utils/helpers.js'
9
- import { highlightCode } from '../highlight.js'
9
+ import { highlightCode, codeBlock } from '../highlight.js'
10
10
  import { marked, renderMarkdownCached } from '../renderer.js'
11
11
  import { slugify, humanize } from 'book-of-spells'
12
12
  import dayjs from 'dayjs'
@@ -197,11 +197,7 @@ export default class NunjucksEngine {
197
197
  env.addFilter('canonical', (page, site) => new nunjucks.runtime.SafeString(buildCanonical(page, site)))
198
198
  env.addFilter('breadcrumb', (page, site, prefix) => new nunjucks.runtime.SafeString(buildBreadcrumb(page, site, prefix)))
199
199
  env.addFilter('groupby', (arr, key, datePart) => groupby(arr, key, datePart))
200
- env.addFilter('highlight', (code, lang) => {
201
- const highlighted = highlightCode(code, lang)
202
- const langClass = lang ? ` language-${lang}` : ''
203
- return new nunjucks.runtime.SafeString(`<pre><code class="hljs${langClass}">${highlighted}</code></pre>`)
204
- })
200
+ env.addFilter('highlight', (code, lang) => new nunjucks.runtime.SafeString(codeBlock(highlightCode(code, lang), lang)))
205
201
  }
206
202
 
207
203
  registerTags(getOutputDir) {
@@ -335,12 +331,7 @@ export class HighlightExtension {
335
331
  }
336
332
 
337
333
  run(context, lang, body) {
338
- const code = body()
339
- const highlighted = highlightCode(code, lang)
340
- const langClass = lang ? ` language-${lang}` : ''
341
- return new nunjucks.runtime.SafeString(
342
- `<pre><code class="hljs${langClass}">${highlighted}</code></pre>`
343
- )
334
+ return new nunjucks.runtime.SafeString(codeBlock(highlightCode(body(), lang), lang))
344
335
  }
345
336
  }
346
337
 
@@ -58,20 +58,53 @@ function escapeHtml(str) {
58
58
  return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
59
59
  }
60
60
 
61
- export function highlightCode(code, language) {
62
- if (language) {
63
- const lang = language.toLowerCase().trim()
64
- if (hljs.getLanguage(lang)) {
65
- return hljs.highlight(code, { language: lang }).value
61
+ // A fence's info string is everything after the backticks — the language plus
62
+ // any meta words (```html preview). Only the first word names the language, so
63
+ // the rest has to come off before a hljs lookup or a `language-` class: with it
64
+ // attached, getLanguage misses and the block silently falls to auto-detection.
65
+ export function fenceLang(info) {
66
+ return String(info || '').trim().split(/\s+/)[0].toLowerCase()
67
+ }
68
+
69
+ // Everything after the language is the fence's own markup for a later stage:
70
+ // a bare word becomes a class (```html preview → code.preview), `key=value`
71
+ // becomes a data attribute (tab=options → data-tab="options"). Values are
72
+ // single tokens — no quotes, no spaces — which keeps the parser a split and is
73
+ // enough for settings; anything longer belongs in the markdown around the fence.
74
+ export function fenceAttrs(info) {
75
+ const [lang, ...rest] = String(info || '').trim().split(/\s+/).filter(Boolean)
76
+ const classes = ['hljs']
77
+ if (lang) classes.push(`language-${lang.toLowerCase().replace(/[^\w-]/g, '')}`)
78
+ let data = ''
79
+ for (const token of rest) {
80
+ const eq = token.indexOf('=')
81
+ if (eq < 0) {
82
+ const name = token.replace(/[^\w-]/g, '')
83
+ if (name) classes.push(name)
84
+ continue
66
85
  }
86
+ const key = token.slice(0, eq).replace(/[^\w-]/g, '')
87
+ if (key) data += ` data-${key}="${escapeHtml(token.slice(eq + 1))}"`
88
+ }
89
+ return ` class="${classes.join(' ')}"${data}`
90
+ }
91
+
92
+ // The one place a highlighted fence becomes html. Every renderer and engine
93
+ // goes through it, so they cannot drift on what a fence produces again.
94
+ export function codeBlock(highlighted, info) {
95
+ return `<pre><code${fenceAttrs(info)}>${highlighted}</code></pre>`
96
+ }
97
+
98
+ export function highlightCode(code, language) {
99
+ const lang = fenceLang(language)
100
+ if (lang && hljs.getLanguage(lang)) {
101
+ return hljs.highlight(code, { language: lang }).value
67
102
  }
68
103
  return hljs.highlightAuto(code).value
69
104
  }
70
105
 
71
106
  export const highlightRenderer = {
72
107
  code({ text, lang }) {
73
- const highlighted = highlightCode(text, lang)
74
- const langClass = lang ? ` language-${escapeHtml(lang)}` : ''
75
- return `<pre><code class="hljs${langClass}">${highlighted}</code></pre>\n`
108
+ return `${codeBlock(highlightCode(text, lang), lang)}\n`
76
109
  }
77
110
  }
@@ -3,7 +3,7 @@ import { markedGithubEmoji } from 'marked-github-emoji'
3
3
  import { markedGithubAlerts } from 'marked-github-alerts'
4
4
  import { markedGithubFootnote } from 'marked-github-footnote'
5
5
  import { slugify } from 'book-of-spells'
6
- import { highlightRenderer, highlightCode } from './highlight.js'
6
+ import { highlightRenderer, highlightCode, codeBlock } from './highlight.js'
7
7
  import { decodeTemplateEntities } from './helpers.js'
8
8
 
9
9
  const RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g
@@ -36,8 +36,7 @@ export const markdownRenderer = {
36
36
  // never splits the delimiters, so it needs no special casing.
37
37
  code({ text, lang }) {
38
38
  const highlighted = mapRawSegments(text, (s) => s && highlightCode(s, lang), (s) => highlightCode(s, lang))
39
- const langClass = lang ? ` language-${lang.replace(/[^\w-]/g, '')}` : ''
40
- return `<pre><code class="hljs${langClass}">${highlighted}</code></pre>\n`
39
+ return `${codeBlock(highlighted, lang)}\n`
41
40
  },
42
41
  // Give every heading a slug id + a permalink anchor. The anchor is empty on
43
42
  // purpose — themes reveal a "#" via `.heading-anchor::before`, so a site with
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poops",
3
3
  "description": "Straightforward, no-bullshit bundler for the web.",
4
- "version": "1.9.7",
4
+ "version": "1.9.8",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "poops.js",