@gjsify/site 0.64.14 → 0.64.15

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
@@ -285,6 +285,30 @@ a typo) fails the build, since no Markdown is ever published. Posts are `BlogPos
285
285
  `article:*` meta, and the landing page shows the newest three of its language. On a site with
286
286
  `support` entries, every post ends with a note linking to the support page.
287
287
 
288
+ ### Code blocks
289
+
290
+ A fenced block is rendered plain — `<pre><code class="language-…">` — because the generator
291
+ carries no highlighter. Colours come from a hook the caller supplies, which is what keeps them in
292
+ the HTML instead of in a script on the page:
293
+
294
+ ```typescript
295
+ import { buildSite } from '@gjsify/site';
296
+ import { createHighlighter } from '@gjsify/site-highlight';
297
+
298
+ const highlighter = await createHighlighter();
299
+ await buildSite({
300
+ root: process.cwd(),
301
+ out: 'dist',
302
+ markdown: { highlight: (code, lang) => highlighter.highlight(code, lang) },
303
+ });
304
+ ```
305
+
306
+ `highlight` receives the block's text and its info string verbatim, and answers either the HTML to
307
+ insert or `undefined` to keep the plain block. The returned HTML is inserted as markup, so the hook
308
+ owns escaping. [`@gjsify/site-highlight`](../site-highlight/README.md) is the ready-made one:
309
+ Shiki on its JavaScript regex engine, the same HTML for light and dark, a Blueprint grammar Shiki
310
+ does not bundle — and a stylesheet that has to be appended after the site's own.
311
+
288
312
  ## Theme
289
313
 
290
314
  | Key | Meaning |
@@ -1,5 +1,6 @@
1
1
  import { type BlogPost, type SiteCommonConfig } from '../site-data/index.js';
2
2
  import { type Shell } from './layout.js';
3
+ import { type MarkdownOptions } from './markdown.js';
3
4
  type Config = Pick<SiteCommonConfig, 'base'>;
4
5
  /** How many posts an app's landing page shows. */
5
6
  export declare const LATEST_POSTS = 3;
@@ -59,6 +60,10 @@ export declare function postKey(post: Pick<BlogPost, 'lang' | 'slug'>): string;
59
60
  export declare function renderBlogFeed(shell: Shell, options: BlogFeedOptions): string;
60
61
  /** Every tag in one language, in first-use order. */
61
62
  export declare function tagsOf(posts: readonly BlogPost[], lang: string): string[];
62
- /** Renders a post's Markdown with its URLs resolved. */
63
- export declare function renderPostBody(config: Config, post: BlogPost, postsDir: string, posts: readonly BlogPost[]): string;
63
+ /**
64
+ * Renders a post's Markdown with its URLs resolved. `markdown` reaches the renderer untouched,
65
+ * except for `resolveUrl`, which the post's own URL resolver supplies — that is what makes a
66
+ * relative image or a link to another post's `.md` file work on the published page.
67
+ */
68
+ export declare function renderPostBody(config: Config, post: BlogPost, postsDir: string, posts: readonly BlogPost[], markdown?: MarkdownOptions): string;
64
69
  export {};
@@ -241,7 +241,11 @@ export function renderBlogFeed(shell, options) {
241
241
  export function tagsOf(posts, lang) {
242
242
  return [...new Set(posts.filter((post) => post.lang === lang).flatMap((post) => post.tags))];
243
243
  }
244
- /** Renders a post's Markdown with its URLs resolved. */
245
- export function renderPostBody(config, post, postsDir, posts) {
246
- return renderMarkdown(post.body, { resolveUrl: postUrlResolver(config, post, postsDir, posts) });
244
+ /**
245
+ * Renders a post's Markdown with its URLs resolved. `markdown` reaches the renderer untouched,
246
+ * except for `resolveUrl`, which the post's own URL resolver supplies — that is what makes a
247
+ * relative image or a link to another post's `.md` file work on the published page.
248
+ */
249
+ export function renderPostBody(config, post, postsDir, posts, markdown = {}) {
250
+ return renderMarkdown(post.body, { ...markdown, resolveUrl: postUrlResolver(config, post, postsDir, posts) });
247
251
  }
@@ -11,5 +11,16 @@
11
11
  export interface MarkdownOptions {
12
12
  /** Rewrites a link href or image src; absolute and `#` URLs are passed in too. */
13
13
  resolveUrl?: (url: string) => string;
14
+ /**
15
+ * Renders one fenced code block instead of the plain `<pre><code>` element — a syntax
16
+ * highlighter, for instance. `lang` is the fence's info string verbatim (empty when the fence
17
+ * names none), so a hook that only knows a few languages can answer `undefined` and leave the
18
+ * block as it is.
19
+ *
20
+ * Returning `undefined` falls back to the plain block. Returned HTML is inserted AS-IS and is
21
+ * therefore never escaped again: the hook owns escaping, because only it knows which parts of
22
+ * its output are markup.
23
+ */
24
+ highlight?: (code: string, lang: string) => string | undefined;
14
25
  }
15
26
  export declare function renderMarkdown(source: string, options?: MarkdownOptions): string;
@@ -308,6 +308,10 @@ function renderBlock(block, options = {}) {
308
308
  case 'hr':
309
309
  return '<hr>';
310
310
  case 'code': {
311
+ const highlighted = options.highlight?.(block.content, block.lang ?? '');
312
+ if (highlighted !== undefined) {
313
+ return highlighted;
314
+ }
311
315
  if (block.lang) {
312
316
  return `<pre><code class="language-${esc(block.lang)}">${esc(block.content)}</code></pre>`;
313
317
  }
package/lib/site.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type I18nResult } from './render/i18n.js';
2
+ import type { MarkdownOptions } from './render/markdown.js';
2
3
  /** Where a build reads from and writes to. */
3
4
  export interface SiteTarget {
4
5
  /** Repository root: the root package.json, the metainfo, the screenshots and the icon are read from here. */
@@ -21,6 +22,12 @@ export interface SiteBuildOptions extends SiteTarget {
21
22
  githubToken?: string;
22
23
  log?: (message: string) => void;
23
24
  warn?: (message: string) => void;
25
+ /**
26
+ * How a blog post's Markdown body is rendered. `resolveUrl` is supplied by the build; the
27
+ * useful field here is `highlight`, which lets a consumer colour code blocks without adding
28
+ * client JavaScript to the page. `undefined` keeps the plain `<pre><code>` output.
29
+ */
30
+ markdown?: MarkdownOptions;
24
31
  /** This package's install, for a caller whose module URL does not reveal it — a bundle. */
25
32
  packageDir?: string;
26
33
  }
package/lib/site.js CHANGED
@@ -127,7 +127,7 @@ async function loadPageCatalogs(config, root, self, warn) {
127
127
  return catalogs;
128
128
  }
129
129
  /** Reads the posts and copies the files next to them to `<out>/blog/`, where the post URLs point. */
130
- async function loadBlog(config, target, drafts) {
130
+ async function loadBlog(config, target, drafts, markdown) {
131
131
  if (!config.blog)
132
132
  return undefined;
133
133
  const dir = resolve(target.root, config.blog.posts);
@@ -136,7 +136,7 @@ async function loadBlog(config, target, drafts) {
136
136
  await mkdir(dirname(join(target.out, 'blog', file)), { recursive: true });
137
137
  await copyFile(join(dir, file), join(target.out, 'blog', file));
138
138
  }
139
- const html = new Map(posts.map((post) => [postKey(post), renderPostBody(config, post, dir, posts)]));
139
+ const html = new Map(posts.map((post) => [postKey(post), renderPostBody(config, post, dir, posts, markdown)]));
140
140
  return { posts, html };
141
141
  }
142
142
  /** Writes the blog of every language and answers its sitemap entries. */
@@ -200,7 +200,7 @@ export async function buildSite(options) {
200
200
  const assets = await stageClientAssets(config.base, target.out, self);
201
201
  const catalogs = await loadPageCatalogs(config, target.root, self, options.warn);
202
202
  const displayFontUrl = await publishDisplayFont(config, target);
203
- const blog = await loadBlog(config, target, options.drafts ?? false);
203
+ const blog = await loadBlog(config, target, options.drafts ?? false, options.markdown);
204
204
  const build = { options, target, cacheDir, assets, catalogs, displayFontUrl, blog };
205
205
  return config.kind === 'app' ? buildAppSite(build) : buildDeveloperSite(config, build);
206
206
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/site",
3
- "version": "0.64.14",
3
+ "version": "0.64.15",
4
4
  "description": "Static landing page generator for a GNOME-style app, rendered from its AppStream metainfo — downloads, screenshots, release notes, support links, no JavaScript required.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -54,21 +54,21 @@
54
54
  "license": "MIT",
55
55
  "dependencies": {
56
56
  "@girs/gdkpixbuf-2.0": "5.4.0",
57
- "@gjsify/console": "^0.64.14",
58
- "@gjsify/crypto": "^0.64.14",
59
- "@gjsify/fs": "^0.64.14",
60
- "@gjsify/module": "^0.64.14",
61
- "@gjsify/os": "^0.64.14",
62
- "@gjsify/path": "^0.64.14",
63
- "@gjsify/process": "^0.64.14",
64
- "@gjsify/url": "^0.64.14",
57
+ "@gjsify/console": "^0.64.15",
58
+ "@gjsify/crypto": "^0.64.15",
59
+ "@gjsify/fs": "^0.64.15",
60
+ "@gjsify/module": "^0.64.15",
61
+ "@gjsify/os": "^0.64.15",
62
+ "@gjsify/path": "^0.64.15",
63
+ "@gjsify/process": "^0.64.15",
64
+ "@gjsify/url": "^0.64.15",
65
65
  "fast-xml-parser": "^5.11.2",
66
66
  "qrcode-generator": "^2.0.4"
67
67
  },
68
68
  "devDependencies": {
69
- "@gjsify/cli": "^0.64.14",
69
+ "@gjsify/cli": "^0.64.15",
70
70
  "@gjsify/node-gi": "file:../../node-gi/node-gi",
71
- "@gjsify/unit": "^0.64.14",
71
+ "@gjsify/unit": "^0.64.15",
72
72
  "@types/node": "^25.9.2",
73
73
  "typescript": "^6.0.3"
74
74
  },