redweb 0.16.3 → 0.16.4

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/index.d.ts CHANGED
@@ -725,10 +725,13 @@ declare module 'redweb' {
725
725
  export function attribute(value: string | number | bigint | boolean): HtmlAttribute;
726
726
  export function url(value: string): HtmlUrl;
727
727
  export function each<Item>(items: readonly Item[], render: (item: Item, index: number) => HtmlFragment): HtmlFragment;
728
+ /** Escaped JS/TS/TSX token markup, including references inside rw-* JSX expressions. */
729
+ export function highlightCode(source: string, language: string): HtmlFragment;
728
730
  export function codeBlock(code: unknown, options?: {
729
731
  language?: string;
730
732
  label?: string;
731
- highlight?: (source: string, language: string) => HtmlFragment;
733
+ /** Defaults to Redweb's JS/TS/TSX highlighter. false keeps escaped plain text. */
734
+ highlight?: false | ((source: string, language: string) => HtmlFragment);
732
735
  }): HtmlFragment;
733
736
 
734
737
  export type LivePageClass = new () => object;
package/index.js CHANGED
@@ -19,7 +19,7 @@ const { Application, defineApp } = require('./src/Application');
19
19
  const { connectedClients, ConnectedClients, ConnectedClient, ClientError } = require('./src/ws/ConnectedClients');
20
20
  const HttpServer = require('./src/http/HttpServer');
21
21
  const HttpsServer = require('./src/http/HttpsServer');
22
- const { action, attribute, codeBlock, component, defineSite, each, exportStatic, html, HtmlRenderer, inject, LiveHtmlServer, LivePage, LiveResource, liveResource, page, resource, start, state, upload, url, view } = require('./src/htmx');
22
+ const { action, attribute, codeBlock, component, defineSite, each, exportStatic, highlightCode, html, HtmlRenderer, inject, LiveHtmlServer, LivePage, LiveResource, liveResource, page, resource, start, state, upload, url, view } = require('./src/htmx');
23
23
  module.exports = {
24
24
  Application,
25
25
  defineApp,
@@ -53,6 +53,7 @@ module.exports = {
53
53
  defineSite,
54
54
  each,
55
55
  exportStatic,
56
+ highlightCode,
56
57
  html,
57
58
  HtmlRenderer,
58
59
  inject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redweb",
3
- "version": "0.16.3",
3
+ "version": "0.16.4",
4
4
  "description": "A small Node.js foundation for HTTP, WebSockets, multiplayer services, and server-rendered HTML",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -39,6 +39,7 @@
39
39
  "default": "./jsx-dev-runtime.js"
40
40
  },
41
41
  "./tsconfig.json": "./config/tsconfig.json",
42
+ "./code-highlight.css": "./styles/code-highlight.css",
42
43
  "./package.json": "./package.json"
43
44
  },
44
45
  "scripts": {
@@ -114,6 +115,7 @@
114
115
  "bin",
115
116
  "config",
116
117
  "src/*",
118
+ "styles",
117
119
  "client.js",
118
120
  "client.d.ts",
119
121
  "contract.js",
@@ -163,7 +165,9 @@
163
165
  "ws": "^8.21.3"
164
166
  },
165
167
  "overrides": {
166
- "express": { "qs": "6.16.0" }
168
+ "express": {
169
+ "qs": "6.16.0"
170
+ }
167
171
  },
168
172
  "devDependencies": {
169
173
  "@types/jest": "^29.5.12",
@@ -0,0 +1,98 @@
1
+ const KEYWORDS = new Set([
2
+ 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue',
3
+ 'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for',
4
+ 'from', 'function', 'if', 'import', 'in', 'instanceof', 'let', 'new',
5
+ 'of', 'return', 'static', 'super', 'switch', 'throw', 'try', 'typeof',
6
+ 'var', 'while', 'yield',
7
+ ]);
8
+ const LITERALS = new Set(['false', 'null', 'true', 'undefined']);
9
+ const LANGUAGES = new Set(['js', 'javascript', 'ts', 'typescript', 'tsx']);
10
+ const identifierStart = character => /[A-Za-z_$]/.test(character);
11
+ const identifierPart = character => /[A-Za-z0-9_$]/.test(character);
12
+
13
+ function actionAttributeBefore(source, brace) {
14
+ let index = brace - 1;
15
+ while (index >= 0 && /\s/.test(source[index])) index -= 1;
16
+ if (source[index] !== '=') return false;
17
+ index -= 1;
18
+ while (index >= 0 && /\s/.test(source[index])) index -= 1;
19
+ const end = index + 1;
20
+ while (index >= 0 && /[\w-]/.test(source[index])) index -= 1;
21
+ return /^rw-[\w-]+$/.test(source.slice(index + 1, end));
22
+ }
23
+
24
+ function quotedEnd(source, start) {
25
+ const quote = source[start];
26
+ let index = start + 1;
27
+ while (index < source.length) {
28
+ if (source[index] === '\\') index += 2;
29
+ else if (source[index++] === quote) break;
30
+ }
31
+ return index;
32
+ }
33
+
34
+ function tokens(source) {
35
+ const result = [];
36
+ let index = 0;
37
+ let actionDepth = 0;
38
+ const push = (kind, end) => {
39
+ result.push({ kind, value: source.slice(index, end) });
40
+ index = end;
41
+ };
42
+ while (index < source.length) {
43
+ if (source.startsWith('//', index)) {
44
+ const newline = source.indexOf('\n', index);
45
+ push('comment', newline < 0 ? source.length : newline);
46
+ continue;
47
+ }
48
+ if (source.startsWith('/*', index)) {
49
+ const close = source.indexOf('*/', index + 2);
50
+ push('comment', close < 0 ? source.length : close + 2);
51
+ continue;
52
+ }
53
+ const character = source[index];
54
+ if (character === '{') {
55
+ if (actionDepth) actionDepth += 1;
56
+ else if (actionAttributeBefore(source, index)) actionDepth = 1;
57
+ push('plain', index + 1);
58
+ continue;
59
+ }
60
+ if (character === '}') {
61
+ if (actionDepth) actionDepth -= 1;
62
+ push('plain', index + 1);
63
+ continue;
64
+ }
65
+ if (character === "'" || character === '"' || character === '`') {
66
+ push('string', quotedEnd(source, index));
67
+ continue;
68
+ }
69
+ if (/\d/.test(character)) {
70
+ const match = /^(?:0[xob][\da-f]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?)/i.exec(source.slice(index));
71
+ push('number', index + match[0].length);
72
+ continue;
73
+ }
74
+ if (identifierStart(character)) {
75
+ let end = index + 1;
76
+ while (end < source.length && identifierPart(source[end])) end += 1;
77
+ const value = source.slice(index, end);
78
+ push(KEYWORDS.has(value) ? 'keyword' : LITERALS.has(value) ? 'literal' : actionDepth ? 'reference' : 'plain', end);
79
+ continue;
80
+ }
81
+ let end = index + 1;
82
+ while (end < source.length && !source.startsWith('//', end) && !source.startsWith('/*', end)
83
+ && !['{', '}', "'", '"', '`'].includes(source[end]) && !/\d/.test(source[end])
84
+ && !identifierStart(source[end])) end += 1;
85
+ push('plain', end);
86
+ }
87
+ return result;
88
+ }
89
+
90
+ function highlightCode(source, language) {
91
+ const { html } = require('./Html');
92
+ if (!LANGUAGES.has(language)) return html`${source}`;
93
+ return html`${tokens(source).map(token => token.kind === 'plain'
94
+ ? html`${token.value}`
95
+ : html`<span class="${`token-${token.kind}`}">${token.value}</span>`)}`;
96
+ }
97
+
98
+ module.exports = { highlightCode };
package/src/htmx/Html.js CHANGED
@@ -123,12 +123,13 @@ function codeBlock(code, options = {}) {
123
123
  throw new TypeError('codeBlock() language must be a safe name of at most 32 characters.');
124
124
  }
125
125
  if (typeof label !== 'string') throw new TypeError('codeBlock() label must be a string.');
126
- if (highlight !== undefined && typeof highlight !== 'function') throw new TypeError('codeBlock() highlight must be a function.');
126
+ if (highlight !== undefined && highlight !== false && typeof highlight !== 'function') throw new TypeError('codeBlock() highlight must be a function or false.');
127
127
  const caption = label ? html`<figcaption>${label}</figcaption>` : html``;
128
128
  let content = isHtml(code) ? code : String(code ?? '');
129
- if (highlight) {
129
+ const highlighter = highlight === false ? undefined : highlight ?? (isHtml(code) ? undefined : require('./CodeHighlight').highlightCode);
130
+ if (highlighter) {
130
131
  if (isHtml(code)) throw new TypeError('codeBlock() cannot highlight an HtmlFragment.');
131
- content = synchronous(highlight(content, language), 'codeBlock() highlight must render synchronously.');
132
+ content = synchronous(highlighter(content, language), 'codeBlock() highlight must render synchronously.');
132
133
  if (!isHtml(content)) throw new TypeError('codeBlock() highlight must return an HtmlFragment.');
133
134
  }
134
135
  return html`<figure class="redweb-code">${caption}<pre><code class="${attribute(`language-${language}`)}">${content}</code></pre></figure>`;
package/src/htmx/index.js CHANGED
@@ -2,10 +2,11 @@ const HtmlRenderer = require('./HtmlRenderer');
2
2
  const LiveHtmlServer = require('./LiveHtmlServer');
3
3
  const LivePage = require('./LivePage');
4
4
  const { attribute, codeBlock, each, html, safeUrl: url } = require('./Html');
5
+ const { highlightCode } = require('./CodeHighlight');
5
6
  const { action, component, inject, page, resource, state, upload, view } = require('./metadata');
6
7
  const { LiveResource, liveResource } = require('./LiveResource');
7
8
  const { start } = require('./start');
8
9
  const { exportStatic } = require('./StaticExporter');
9
10
  const { defineSite } = require('./StaticSite');
10
11
 
11
- module.exports = { action, attribute, codeBlock, component, defineSite, each, exportStatic, html, HtmlRenderer, inject, LiveHtmlServer, LivePage, LiveResource, liveResource, page, resource, start, state, upload, url, view };
12
+ module.exports = { action, attribute, codeBlock, component, defineSite, each, exportStatic, highlightCode, html, HtmlRenderer, inject, LiveHtmlServer, LivePage, LiveResource, liveResource, page, resource, start, state, upload, url, view };
@@ -0,0 +1,16 @@
1
+ .redweb-code {
2
+ color: #e5e7eb;
3
+ background: #111827;
4
+ border-radius: 0.75rem;
5
+ overflow: hidden;
6
+ }
7
+ .redweb-code pre {
8
+ overflow-x: auto;
9
+ padding: 1rem;
10
+ }
11
+ .redweb-code .token-comment { color: #7dd3fc; }
12
+ .redweb-code .token-keyword { color: #93c5fd; }
13
+ .redweb-code .token-literal { color: #c4b5fd; }
14
+ .redweb-code .token-number { color: #fcd34d; }
15
+ .redweb-code .token-string { color: #fca5a5; }
16
+ .redweb-code .token-reference { color: #a7f3d0; }