ferrovue 0.15.0 → 0.15.2
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 +2 -2
- package/dist/config.d.ts +5 -3
- package/dist/whitespace.d.ts +9 -5
- package/dist/whitespace.js +75 -10
- package/package.json +1 -1
- package/schema.json +2 -2
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ the configuration at your project root yourself:
|
|
|
57
57
|
|
|
58
58
|
```json
|
|
59
59
|
{
|
|
60
|
-
"$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.15.
|
|
60
|
+
"$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.15.2/schema.json",
|
|
61
61
|
"components": "client/components",
|
|
62
62
|
"out": "src/generated"
|
|
63
63
|
}
|
|
@@ -242,7 +242,7 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
|
|
|
242
242
|
| Lists | `v-for` over arrays (of strings, numbers, objects or child props), array literals and number ranges (`n in 5`); with an index; with destructured items (`{ id, name } in rows`); nested; on `<template>`. `.includes()` (a number looked for in a list of numbers whether either is a `number` or a `Float`, as JavaScript compares them), `.join()`, and `.filter()`, `.map()`, `.some()`, `.every()`, `.find()`, `.findIndex()`, `.findLast()`, `.findLastIndex()` with an arrow function of the item (a name, or destructured) and its index, whose body is an expression or a body of statements (see Statements), narrowing inside as outside, or with `Boolean`; `.filter(Boolean)` of a list or an array literal whose items may be `null` or `undefined`, which keeps the ones present and truthy as a list of the values (`[name, title, city].filter(Boolean).join(" · ")`); `Boolean(x)`; `.slice()`; an item by its index, `list[i]` with an integer `i` and `list.at(i)` (from the end when negative), `undefined` outside the list and narrowed as other optional values are (`v-if="rows[0]"`); `.indexOf()` and `.lastIndexOf()` of a string, number or boolean, compared with `===` (`NaN` found nowhere, `-0` found as `0`); `.concat()` of lists of one item type (numbers whether `number` or `Float`); `.reduce()` and `.reduceRight()` with an initial value, into a string, a number or a boolean; `.toReversed()`, and `.toSorted()` by `String(x)` as JavaScript sorts without a comparator (`[10, 9, 1]` to `[1, 10, 9]`) or by a comparator `(a, b) => a - b`, `a.k - b.k` of integers or `?:` over `<` and `>` of integers or strings, stable as JavaScript's; `[...list].sort(…)` and `[...list].reverse()`; spread in array literals (`[...a, x, ...b]`) of one item type; `.flat()` of lists of lists, to a literal depth or `Infinity`, and `.flatMap()` with an arrow function that gives a list (flattened one level) or a value (kept as `.map()` keeps it); chained and nested; a computed list read by `v-for`, `.join()`, `.length`, `.includes()`, kept by `computed`, or handed to a child |
|
|
243
243
|
| Dictionaries | `v-for="(value, key, index) in r"` over a `Record<string, T>`, and `([key, value], index) in Object.entries(r)`, in JavaScript's order: keys that are array indices first, in numeric order, then the others as the props give them; `Object.keys(r)`, `Object.values(r)` (of a record of lists too, which `.flat()` joins), `Object.hasOwn(r, key)`, and `Object.entries(r)` read by the list methods, whose arrow functions destructure each pair (`([key, value]) => …`, `([, value]) => …`, `([key, { id }]) => …`) or read its parts (`pair[0]`, `pair[1]`), and walked by `v-for="[key, value] in Object.entries(r).filter(…)"`, kept by `computed` and sorted by a comparator of either part; handed to a child |
|
|
244
244
|
| HTML comments | left out, as Vue's production build leaves them out, or kept with `comments` or `"isProduction": false`, as on the dev server: written as Vue writes them, at the root, between roots, inside elements and in slot content |
|
|
245
|
-
| Whitespace | text condensed as Vue condenses it, or with `"whitespace": "trim-edges"` also trimmed where it meets
|
|
245
|
+
| Whitespace | text condensed as Vue condenses it, or with `"whitespace": "trim-edges"` also trimmed where it meets a block or inline-block element's tags across a line break and spaced between inline elements and components across one, the client compiled alike with `trimEdges`: in elements, components and `<template v-slot>`, keeping `<pre>`, `<textarea>`, `v-pre` and a static `white-space: pre*` style. See [`generated_code`](https://docs.rs/ferrovue/latest/ferrovue/guide/generated_code/index.html#whitespace) |
|
|
246
246
|
| Attributes | static and bound attributes, boolean attributes, `:hidden`, `data-*` and `aria-*`, `v-bind` objects |
|
|
247
247
|
| `class` | strings, arrays, objects (`{ active: on }`, computed keys), `cond && "x"`, `cond ? "x" : null`, merged with a static `class` |
|
|
248
248
|
| `style` | objects (camelCase or kebab-case keys, `--custom` properties), arrays of objects, strings, merged with a static `style`, and `v-show`; later values override earlier ones as in Vue. A global `<style>` block is allowed |
|
package/dist/config.d.ts
CHANGED
|
@@ -104,9 +104,11 @@ export interface Config {
|
|
|
104
104
|
* `false` and left out when it is `true`. */
|
|
105
105
|
comments?: boolean;
|
|
106
106
|
/** How the text of templates is spaced: `"condense"` (the default) as Vue spaces it, or
|
|
107
|
-
* `"trim-edges"`, which also trims the whitespace where text meets
|
|
108
|
-
*
|
|
109
|
-
*
|
|
107
|
+
* `"trim-edges"`, which also trims the whitespace holding a line break where text meets a block
|
|
108
|
+
* or inline-block element's tags and renders a line break between two inline elements as a
|
|
109
|
+
* space. The client
|
|
110
|
+
* compiles the same way with `trimEdges` from `ferrovue/whitespace` in `@vitejs/plugin-vue`'s
|
|
111
|
+
* `template.compilerOptions.nodeTransforms`. */
|
|
110
112
|
whitespace?: WhitespaceMode;
|
|
111
113
|
/** CSS modules (`<style module>`): how Vite's `css.modules` names their classes, which ferrovue
|
|
112
114
|
* computes as postcss-modules does. Without it, `<style module>` is refused. */
|
package/dist/whitespace.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/** A node transform for `@vitejs/plugin-vue`'s `template.compilerOptions.nodeTransforms` that
|
|
2
|
-
* trims the whitespace at the start and end of
|
|
3
|
-
* tags, as `"whitespace": "trim-edges"`
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* trims the whitespace at the start and end of a block or inline-block element's text, where it
|
|
3
|
+
* meets the element's tags, when that whitespace holds a line break, as `"whitespace": "trim-edges"`
|
|
4
|
+
* makes ferrovue's Rust render it. An inline element's edges keep Vue's one space, and a space
|
|
5
|
+
* typed on the same line as the tag stays. A line break between two inline elements, components,
|
|
6
|
+
* `<slot>`s or `<template>`s, such as `<span>` and `<a>`, renders as one space, as a formatter
|
|
7
|
+
* reads it, also at the template's root and across a comment.
|
|
8
|
+
* It trims the content of block and inline-block elements, components and `<template v-slot>`, and
|
|
9
|
+
* leaves `<pre>`, `<textarea>`, `v-pre` and an element whose `style` sets `white-space: pre`,
|
|
10
|
+
* `pre-wrap`, `pre-line` or `break-spaces` as they are, with everything inside them. */
|
|
7
11
|
export declare function trimEdges(node: object): void;
|
package/dist/whitespace.js
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
const ROOT = 0;
|
|
2
2
|
const ELEMENT = 1;
|
|
3
3
|
const TEXT = 2;
|
|
4
|
+
const COMMENT = 3;
|
|
4
5
|
const PLAIN = 0;
|
|
5
6
|
const COMPONENT = 1;
|
|
7
|
+
const SLOT = 2;
|
|
6
8
|
const TEMPLATE = 3;
|
|
7
9
|
const LEADING = /^[ \t\n\f\r]+/;
|
|
8
10
|
const TRAILING = /[ \t\n\f\r]+$/;
|
|
11
|
+
const LINE_BREAK = /[\n\r]/;
|
|
12
|
+
const BLANK = /^[ \t\n\f\r]+$/;
|
|
13
|
+
const HTML = 0;
|
|
14
|
+
const SPACELESS_TAGS = new Set([
|
|
15
|
+
"address article aside blockquote body br caption center col colgroup dd details dialog dir div dl dt fieldset figcaption figure footer form",
|
|
16
|
+
"h1 h2 h3 h4 h5 h6 head header hgroup hr html legend li link listing main menu meta nav noscript ol optgroup option p param plaintext pre",
|
|
17
|
+
"script search section source style summary table tbody td template tfoot th thead title tr track ul xmp area base basefont datalist noembed noframes rp",
|
|
18
|
+
]
|
|
19
|
+
.join(" ")
|
|
20
|
+
.split(" "));
|
|
21
|
+
const INLINE_BLOCK_TAGS = new Set(["audio", "button", "meter", "object", "progress", "select", "video"]);
|
|
22
|
+
const COMMENTS = /<!--[\s\S]*?(?:-->|$)/g;
|
|
9
23
|
const KEPT_TAGS = new Set(["pre", "textarea"]);
|
|
10
24
|
const PRE_STYLE = /(?:^|;)\s*white-space\s*:\s*(?:pre|pre-wrap|pre-line|break-spaces)\s*(?:!important\s*)?(?:;|$)/i;
|
|
11
25
|
const OPEN_TAG = /^<[^\s/>]+(?:[^"'>]|"[^"]*"|'[^']*')*/;
|
|
12
26
|
const QUOTED = /"[^"]*"|'[^']*'/g;
|
|
13
27
|
const V_PRE = /\sv-pre(?=[\s/=]|$)/;
|
|
28
|
+
const PLACELESS_COMPONENTS = new Set(["teleport", "keepalive", "suspense", "transition", "transitiongroup", "clientonly"]);
|
|
14
29
|
const MARK = Symbol.for("ferrovue.whitespace");
|
|
15
30
|
function keeps(el) {
|
|
16
31
|
if (KEPT_TAGS.has(el.tag.toLowerCase()))
|
|
@@ -21,38 +36,88 @@ function keeps(el) {
|
|
|
21
36
|
return V_PRE.test(open.replace(QUOTED, '""'));
|
|
22
37
|
}
|
|
23
38
|
function trims(el) {
|
|
24
|
-
if (el.tagType === PLAIN
|
|
39
|
+
if (el.tagType === PLAIN) {
|
|
40
|
+
const tag = el.tag.toLowerCase();
|
|
41
|
+
return el.ns !== HTML || SPACELESS_TAGS.has(tag) || INLINE_BLOCK_TAGS.has(tag);
|
|
42
|
+
}
|
|
43
|
+
if (el.tagType === COMPONENT)
|
|
25
44
|
return true;
|
|
26
45
|
return el.tagType === TEMPLATE && el.props.some((p) => p.type === 7 && p.name === "slot");
|
|
27
46
|
}
|
|
47
|
+
function breaks(text, edge) {
|
|
48
|
+
return LINE_BREAK.test(edge.exec(text.loc.source)?.[0] ?? "");
|
|
49
|
+
}
|
|
28
50
|
function trimChildren(el) {
|
|
29
51
|
const children = el.children;
|
|
30
52
|
const first = children[0];
|
|
31
|
-
if (first?.type === TEXT)
|
|
53
|
+
if (first?.type === TEXT && breaks(first, LEADING))
|
|
32
54
|
first.content = first.content.replace(LEADING, "");
|
|
33
55
|
const last = children.at(-1);
|
|
34
|
-
if (last?.type === TEXT)
|
|
56
|
+
if (last?.type === TEXT && breaks(last, TRAILING))
|
|
35
57
|
last.content = last.content.replace(TRAILING, "");
|
|
36
58
|
el.children = children.filter((c) => c.type !== TEXT || c.content !== "");
|
|
37
59
|
}
|
|
60
|
+
function inline(node) {
|
|
61
|
+
if (node.type !== ELEMENT)
|
|
62
|
+
return false;
|
|
63
|
+
if (node.tagType === COMPONENT)
|
|
64
|
+
return !PLACELESS_COMPONENTS.has(node.tag.replaceAll("-", "").toLowerCase());
|
|
65
|
+
if (node.tagType === SLOT)
|
|
66
|
+
return true;
|
|
67
|
+
if (node.tagType === TEMPLATE)
|
|
68
|
+
return !node.props.some((p) => p.type === 7 && p.name === "slot");
|
|
69
|
+
if (node.tagType !== PLAIN)
|
|
70
|
+
return false;
|
|
71
|
+
const tag = node.tag.toLowerCase();
|
|
72
|
+
return (node.ns === HTML || tag === "svg" || tag === "math") && !SPACELESS_TAGS.has(tag);
|
|
73
|
+
}
|
|
74
|
+
function spaceInline(node, source, start) {
|
|
75
|
+
const children = node.children;
|
|
76
|
+
const spaced = [];
|
|
77
|
+
let prev;
|
|
78
|
+
let after = 0;
|
|
79
|
+
children.forEach((child) => {
|
|
80
|
+
if (child.type === COMMENT) {
|
|
81
|
+
spaced.push(child);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (prev !== undefined && inline(prev) && inline(child)) {
|
|
85
|
+
const gap = source.slice(prev.loc.end.offset - start, child.loc.start.offset - start).replace(COMMENTS, "");
|
|
86
|
+
if (BLANK.test(gap) && LINE_BREAK.test(gap))
|
|
87
|
+
spaced.splice(after, 0, { type: TEXT, content: " ", loc: { source: gap, start: prev.loc.end, end: child.loc.start } });
|
|
88
|
+
}
|
|
89
|
+
spaced.push(child);
|
|
90
|
+
prev = child;
|
|
91
|
+
after = spaced.length;
|
|
92
|
+
});
|
|
93
|
+
node.children = spaced;
|
|
94
|
+
}
|
|
38
95
|
function walk(node) {
|
|
39
96
|
for (const child of node.children ?? []) {
|
|
40
97
|
if (child.type !== ELEMENT || keeps(child))
|
|
41
98
|
continue;
|
|
42
99
|
if (trims(child))
|
|
43
100
|
trimChildren(child);
|
|
101
|
+
if (child.ns === HTML)
|
|
102
|
+
spaceInline(child, child.loc.source, child.loc.start.offset);
|
|
44
103
|
walk(child);
|
|
45
104
|
}
|
|
46
105
|
}
|
|
47
106
|
/** A node transform for `@vitejs/plugin-vue`'s `template.compilerOptions.nodeTransforms` that
|
|
48
|
-
* trims the whitespace at the start and end of
|
|
49
|
-
* tags, as `"whitespace": "trim-edges"`
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
107
|
+
* trims the whitespace at the start and end of a block or inline-block element's text, where it
|
|
108
|
+
* meets the element's tags, when that whitespace holds a line break, as `"whitespace": "trim-edges"`
|
|
109
|
+
* makes ferrovue's Rust render it. An inline element's edges keep Vue's one space, and a space
|
|
110
|
+
* typed on the same line as the tag stays. A line break between two inline elements, components,
|
|
111
|
+
* `<slot>`s or `<template>`s, such as `<span>` and `<a>`, renders as one space, as a formatter
|
|
112
|
+
* reads it, also at the template's root and across a comment.
|
|
113
|
+
* It trims the content of block and inline-block elements, components and `<template v-slot>`, and
|
|
114
|
+
* leaves `<pre>`, `<textarea>`, `v-pre` and an element whose `style` sets `white-space: pre`,
|
|
115
|
+
* `pre-wrap`, `pre-line` or `break-spaces` as they are, with everything inside them. */
|
|
53
116
|
export function trimEdges(node) {
|
|
54
117
|
const n = node;
|
|
55
|
-
if (n.type
|
|
56
|
-
|
|
118
|
+
if (n.type !== ROOT)
|
|
119
|
+
return;
|
|
120
|
+
spaceInline(n, n.source ?? n.loc.source, n.source === undefined ? n.loc.start.offset : 0);
|
|
121
|
+
walk(n);
|
|
57
122
|
}
|
|
58
123
|
Object.defineProperty(trimEdges, MARK, { value: "trim-edges" });
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -311,8 +311,8 @@
|
|
|
311
311
|
"type": "boolean"
|
|
312
312
|
},
|
|
313
313
|
"whitespace": {
|
|
314
|
-
"description": "How the text of templates is spaced: `\"condense\"` (the default) as Vue spaces it, or `\"trim-edges\"`, which also trims the whitespace where text meets
|
|
315
|
-
"markdownDescription": "How the text of templates is spaced: `\"condense\"` (the default) as Vue spaces it, or `\"trim-edges\"`, which also trims the whitespace where text meets
|
|
314
|
+
"description": "How the text of templates is spaced: `\"condense\"` (the default) as Vue spaces it, or `\"trim-edges\"`, which also trims the whitespace holding a line break where text meets a block or inline-block element's tags and renders a line break between two inline elements as a space. The client compiles the same way with `trimEdges` from `ferrovue/whitespace` in `@vitejs/plugin-vue`'s `template.compilerOptions.nodeTransforms`.",
|
|
315
|
+
"markdownDescription": "How the text of templates is spaced: `\"condense\"` (the default) as Vue spaces it, or `\"trim-edges\"`, which also trims the whitespace holding a line break where text meets a block or inline-block element's tags and renders a line break between two inline elements as a space. The client compiles the same way with `trimEdges` from `ferrovue/whitespace` in `@vitejs/plugin-vue`'s `template.compilerOptions.nodeTransforms`.",
|
|
316
316
|
"type": "string",
|
|
317
317
|
"enum": [
|
|
318
318
|
"condense",
|