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/CHANGELOG.md +5 -0
- package/README.md +3 -3
- package/docs/CLI.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/GETTING_STARTED.md +4 -4
- package/docs/LIVE_HTML.md +1 -1
- package/docs/MIGRATION.md +1 -1
- package/docs/RELEASE_TRUST.md +4 -4
- package/docs/SOCKET_CONTRACTS.md +1 -1
- package/docs/generated.json +226 -222
- package/docs/reference.json +6 -2
- package/docs/releases/0.16.4.json +2290 -0
- package/index.d.ts +4 -1
- package/index.js +2 -1
- package/package.json +6 -2
- package/src/htmx/CodeHighlight.js +98 -0
- package/src/htmx/Html.js +4 -3
- package/src/htmx/index.js +2 -1
- package/styles/code-highlight.css +16 -0
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
|
-
|
|
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
|
+
"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": {
|
|
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
|
-
|
|
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(
|
|
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; }
|