@dataimago/css-neutral 0.1.0-alpha.12 → 0.1.0-alpha.13
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 +106 -0
- package/dist/bin/dataimago-sync.mjs +281 -19
- package/dist/dataviz/dataimago-dataviz.css +1 -1
- package/dist/dataviz/dataimago-dataviz.json +2 -2
- package/dist/dataviz/dataimago-dataviz.tex +1 -1
- package/dist/dataviz/dataimago_dataviz.R +1 -1
- package/dist/fonts/cyrillic/OFL-NotoSans.txt +93 -0
- package/dist/fonts/cyrillic/OFL-NotoSansMono.txt +93 -0
- package/dist/fonts/cyrillic/fonts.css +27 -0
- package/dist/fonts/cyrillic/noto-sans-italic.woff2 +0 -0
- package/dist/fonts/cyrillic/noto-sans-mono.woff2 +0 -0
- package/dist/fonts/cyrillic/noto-sans.woff2 +0 -0
- package/dist/fonts/cyrillic-ext/OFL-NotoSans.txt +93 -0
- package/dist/fonts/cyrillic-ext/OFL-NotoSansMono.txt +93 -0
- package/dist/fonts/cyrillic-ext/fonts.css +27 -0
- package/dist/fonts/cyrillic-ext/noto-sans-italic.woff2 +0 -0
- package/dist/fonts/cyrillic-ext/noto-sans-mono.woff2 +0 -0
- package/dist/fonts/cyrillic-ext/noto-sans.woff2 +0 -0
- package/dist/fonts/greek/OFL-NotoSans.txt +93 -0
- package/dist/fonts/greek/OFL-NotoSansMono.txt +93 -0
- package/dist/fonts/greek/fonts.css +27 -0
- package/dist/fonts/greek/noto-sans-italic.woff2 +0 -0
- package/dist/fonts/greek/noto-sans-mono.woff2 +0 -0
- package/dist/fonts/greek/noto-sans.woff2 +0 -0
- package/dist/fonts/greek-ext/OFL-NotoSans.txt +93 -0
- package/dist/fonts/greek-ext/OFL-NotoSansMono.txt +93 -0
- package/dist/fonts/greek-ext/fonts.css +27 -0
- package/dist/fonts/greek-ext/noto-sans-italic.woff2 +0 -0
- package/dist/fonts/greek-ext/noto-sans-mono.woff2 +0 -0
- package/dist/fonts/greek-ext/noto-sans.woff2 +0 -0
- package/dist/fonts/lang.css +181 -0
- package/dist/fonts/latin/OFL-NotoSans.txt +93 -0
- package/dist/fonts/latin/OFL-NotoSansMono.txt +93 -0
- package/dist/fonts/latin/fonts.css +27 -0
- package/dist/fonts/latin/noto-sans-italic.woff2 +0 -0
- package/dist/fonts/latin/noto-sans-mono.woff2 +0 -0
- package/dist/fonts/latin/noto-sans.woff2 +0 -0
- package/dist/fonts/latin-ext/OFL-NotoSans.txt +93 -0
- package/dist/fonts/latin-ext/OFL-NotoSansMono.txt +93 -0
- package/dist/fonts/latin-ext/fonts.css +27 -0
- package/dist/fonts/latin-ext/noto-sans-italic.woff2 +0 -0
- package/dist/fonts/latin-ext/noto-sans-mono.woff2 +0 -0
- package/dist/fonts/latin-ext/noto-sans.woff2 +0 -0
- package/dist/fonts/math/OFL-NotoSansMath.txt +93 -0
- package/dist/fonts/math/fonts.css +8 -0
- package/dist/fonts/math/noto-sans-math.woff2 +0 -0
- package/dist/fonts/vietnamese/OFL-NotoSans.txt +93 -0
- package/dist/fonts/vietnamese/OFL-NotoSansMono.txt +93 -0
- package/dist/fonts/vietnamese/fonts.css +27 -0
- package/dist/fonts/vietnamese/noto-sans-italic.woff2 +0 -0
- package/dist/fonts/vietnamese/noto-sans-mono.woff2 +0 -0
- package/dist/fonts/vietnamese/noto-sans.woff2 +0 -0
- package/dist/latex/dataimago-tokens.sty +14 -2
- package/dist/outputs.json +54 -8
- package/dist/quarto/neutral-dark.scss +14 -4
- package/dist/quarto/neutral-light.scss +14 -4
- package/dist/tailwind-preset.js +2 -4
- package/dist/tokens.css +2 -2
- package/dist/tokens.scss +2 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -67,6 +67,112 @@ in CI: it fails on any drift. The shipped outputs are listed in `dist/outputs.js
|
|
|
67
67
|
(e.g. `https://unpkg.com/@dataimago/css-neutral@<version>/dist/outputs.json`). See
|
|
68
68
|
[`wiki/patterns/new-consumer-checklist.md`](../../wiki/patterns/new-consumer-checklist.md).
|
|
69
69
|
|
|
70
|
+
## Fonts
|
|
71
|
+
|
|
72
|
+
The package ships the token fonts as WOFF2 files, so a site or document does not depend on Google's CDN. Everything is under `dist/fonts/`, one directory per script, each with a `fonts.css` of `@font-face` rules and the licence text:
|
|
73
|
+
|
|
74
|
+
- Noto Sans, upright and italic, variable in weight (100 to 900), width pinned at 100.
|
|
75
|
+
- Noto Sans Mono, upright only, variable in weight (100 to 900).
|
|
76
|
+
- Noto Sans Math, one static weight (400), for equations.
|
|
77
|
+
|
|
78
|
+
Directories: latin, latin-ext, greek, greek-ext, cyrillic, cyrillic-ext, vietnamese, math. Each `@font-face` uses `font-display: swap`, a weight range read from the font file, and a `unicode-range` on the subset files, so a browser downloads only the scripts a page renders. There is no `local()` source and no preload. Subsets keep every layout feature the kept text uses; `lnum` and `tnum` work in Noto Sans. Noto Sans Mono's digits are monospaced and lining by design, and it has no figure features. `dist/fonts/lang.css` holds the language rules (below). The licence is OFL-1.1; `dist/fonts/` carries each licence file.
|
|
79
|
+
|
|
80
|
+
This package omits Josefin Sans: its tokens do not name it. `@dataimago/css` ships it.
|
|
81
|
+
|
|
82
|
+
Combining marks: the `latin` and `latin-ext` files also carry the whole combining-diacritics block (U+0300-036F), a documented addition to Google's ranges, so decomposed accents (for example e + U+0301, or o + U+031B) draw from the same file as their base letter.
|
|
83
|
+
|
|
84
|
+
### Using them
|
|
85
|
+
|
|
86
|
+
`dist/fonts/**` is listed in `dist/outputs.json`, so `dataimago-sync` copies it. A key ending in `/` maps a whole directory to a destination ending in `/`.
|
|
87
|
+
|
|
88
|
+
**A web page.** Map every script you ship and `fonts/lang.css`, then link the CSS files. The browser fetches only what the page renders.
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{ "package": "@dataimago/css-neutral", "version": "<exact version>",
|
|
92
|
+
"outputs": { "fonts/latin/": "assets/fonts/latin/", "fonts/latin-ext/": "assets/fonts/latin-ext/",
|
|
93
|
+
"fonts/greek/": "assets/fonts/greek/", "fonts/greek-ext/": "assets/fonts/greek-ext/",
|
|
94
|
+
"fonts/cyrillic/": "assets/fonts/cyrillic/", "fonts/cyrillic-ext/": "assets/fonts/cyrillic-ext/",
|
|
95
|
+
"fonts/vietnamese/": "assets/fonts/vietnamese/",
|
|
96
|
+
"fonts/math/": "assets/fonts/math/", "fonts/lang.css": "assets/fonts/" } }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**A self-contained Quarto output** (`embed-resources: true`). Map only the scripts the work's language needs, for example `fonts/latin/` and `fonts/latin-ext/` (add `fonts/vietnamese/` for Vietnamese text), plus `fonts/math/` if the work has equations and `fonts/lang.css`. List each CSS file under `css:` in `_quarto.yml` and set `html-math-method: mathml`. Quarto then embeds just the files those rules reach.
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
format:
|
|
103
|
+
html:
|
|
104
|
+
embed-resources: true
|
|
105
|
+
html-math-method: mathml
|
|
106
|
+
css:
|
|
107
|
+
- assets/fonts/latin/fonts.css
|
|
108
|
+
- assets/fonts/latin-ext/fonts.css
|
|
109
|
+
- assets/fonts/vietnamese/fonts.css
|
|
110
|
+
- assets/fonts/math/fonts.css
|
|
111
|
+
- assets/fonts/lang.css
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**A translated work in a script the package does not ship** (Arabic, Hebrew, Devanagari, and so on). `lang.css` already names the family. Load it from Google Fonts on the web, or make the files yourself (see "Making a script the package does not ship") and keep them in the work's own repository.
|
|
115
|
+
|
|
116
|
+
### Other languages
|
|
117
|
+
|
|
118
|
+
Text in another language must carry a `lang` attribute, or the browser applies its own fallback. In Quarto write `[text]{lang=ar}` for a span, a fenced div `::: {lang=ar}` for a block, or `lang: ar` in the front matter for a whole work. `lang.css` maps each language to a family by `:lang()` and rebinds the display variables on headings and navigation only; the brand mark, code and maths are left alone. The tags `zh-CN`, `zh-SG` and bare `zh` map to Noto Sans SC (bare `zh` is treated as Simplified); `zh-TW`, `zh-HK` and `zh-MO` map to Noto Sans TC. Tag Traditional text `zh-Hant` or a Traditional region.
|
|
119
|
+
|
|
120
|
+
### Maths
|
|
121
|
+
|
|
122
|
+
Equations use native MathML in `Noto Sans Math` (`font.family.math` is `Noto Sans Math, math`, so a reader without the font gets the browser's maths face). Set `html-math-method: mathml`. MathJax and KaTeX bring their own fonts, and with `embed-resources` Quarto fetches them from the network unless `self-contained-math` is set. A property that chooses one of them is outside this contract.
|
|
123
|
+
|
|
124
|
+
### Synthesised oblique
|
|
125
|
+
|
|
126
|
+
Noto Sans Mono ships upright only. Italic text in them is the browser's synthesised oblique. Noto Sans has a true italic.
|
|
127
|
+
|
|
128
|
+
### Preloading
|
|
129
|
+
|
|
130
|
+
Preloading is yours to opt into; the package does not. For the Latin body file, in the page head:
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<link rel="preload" as="font" type="font/woff2" href="/assets/fonts/latin/noto-sans.woff2" crossorigin>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`crossorigin` is required even for a same-origin font, or the browser fetches it twice.
|
|
137
|
+
|
|
138
|
+
### Caching
|
|
139
|
+
|
|
140
|
+
File names are stable and change only when a family changes, so serve them with revalidation. That is the default on the hosts these properties use (confirmed 2026-10-31): GitHub Pages sends `cache-control: max-age=600` with an ETag, and Vercel serves static files with `public, max-age=0, must-revalidate`. Do not set a long immutable lifetime.
|
|
141
|
+
|
|
142
|
+
### The Google alternative
|
|
143
|
+
|
|
144
|
+
A project that does not sync files can link Google's CDN instead. The house query is shown for reference; css-neutral ships no Josefin Sans, so drop the `family=Josefin+Sans:wght@100..700&` part:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@100..700&family=Noto+Sans:ital,wdth,wght@0,100,100..900;1,100,100..900&family=Noto+Sans+Math&family=Noto+Sans+Mono:wght@100..900&display=swap
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Add `lang.css` as well. This loads full variable files from Google rather than the subsets, so a self-contained Quarto output becomes much larger. The Google route serves Google's own ranges, so decomposed accents using U+0302, U+0306 or U+031B fall back there (and on sites built from the Quarto website template, which uses that route); the shipped files carry the combining marks in the Latin files.
|
|
151
|
+
|
|
152
|
+
### Making a script the package does not ship
|
|
153
|
+
|
|
154
|
+
`subset.mjs` in the dataimago-design repository has an out mode that fetches a pinned upstream file, checks its digest, subsets it and writes `<script>/<family>.woff2`, `<script>/fonts.css` and the licence into a directory outside `packages/`. It refuses a family whose licence reserves the font name.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
node tools/fonts/subset.mjs --out ../my-work/fonts \
|
|
158
|
+
--upstream <url pinned to a tag or commit> --sha256 <hex> --script arabic \
|
|
159
|
+
[--unicode-range "U+0600-06FF, U+200C-200E"] [--style normal|italic] \
|
|
160
|
+
--licence-url <url> --licence-sha256 <hex> \
|
|
161
|
+
[--member <path in archive> --member-sha256 <hex>] \
|
|
162
|
+
[--licence-member <path> --licence-member-sha256 <hex>]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Without the tool, fontTools (the repository pins Python 3.13 and `tools/fonts/requirements.txt`) does the same in two steps. Pin the width axis if the font has one, then subset keeping every layout feature:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
fonttools varLib.instancer NotoSansArabic[wdth,wght].ttf wdth=100 --no-recalc-timestamp -o instanced.ttf
|
|
169
|
+
pyftsubset instanced.ttf --unicodes="U+0600-06FF,U+200C-200E" --layout-features='*' \
|
|
170
|
+
--name-IDs='*' --name-languages='*' --notdef-outline --no-recalc-timestamp \
|
|
171
|
+
--flavor=woff2 --output-file=noto-sans-arabic.woff2
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
To wrap a file as WOFF2 without subsetting: `fonttools ttLib.woff2 compress -o out.woff2 in.ttf`. (The repository's tool also turns off table transforms so the tables stay byte-identical; the command line does not, so the bytes can differ while the font is the same.) Write the `@font-face` rule with `font-display: swap`, the font's own weight range and `unicode-range`, and no `local()`.
|
|
175
|
+
|
|
70
176
|
## Architectural notes
|
|
71
177
|
|
|
72
178
|
Variant A of the two-tier identity pattern — see [`wiki/decisions/tokens-neutral-sibling.md`](../../wiki/decisions/tokens-neutral-sibling.md) for the full rationale, including the iteration-4 deprecation target (Variant B: neutral-as-base + `@dataimago/dataimago-theme` overlay).
|
|
@@ -27,6 +27,10 @@ import path from 'node:path';
|
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
28
|
|
|
29
29
|
const LOCAL = 'local-unreleased';
|
|
30
|
+
// Extensions whose files carry a stamp (mirrors kindOf in tools/formats/build-outputs.mjs; the
|
|
31
|
+
// self-test pins the two together). Everything else is binary: copied byte for byte, never stamped.
|
|
32
|
+
const TEXT_EXT = ['.scss', '.css', '.sty', '.tex', '.r', '.json', '.md', '.qmd', '.yml', '.js', '.lua', '.template'];
|
|
33
|
+
const isText = (distPath) => TEXT_EXT.includes(path.extname(distPath.replace(/\.template$/, '')).toLowerCase());
|
|
30
34
|
|
|
31
35
|
class ConfigError extends Error {}
|
|
32
36
|
|
|
@@ -46,6 +50,11 @@ const USAGE = `dataimago-sync — copy a pinned @dataimago release's design file
|
|
|
46
50
|
dataimago-sync --config <file> read design.json from <file>
|
|
47
51
|
dataimago-sync --help this text
|
|
48
52
|
|
|
53
|
+
An outputs key ending in "/" copies every file the release lists under it into a destination
|
|
54
|
+
that also ends in "/". That directory is owned by the output; files in it the release does not
|
|
55
|
+
list are reported (--check fails on them) but never deleted. Binary files (fonts, licence text)
|
|
56
|
+
are copied byte for byte and carry no stamp.
|
|
57
|
+
|
|
49
58
|
Run it from the pinned release: npx -p @dataimago/css@<version> dataimago-sync`;
|
|
50
59
|
|
|
51
60
|
const FLAGS = new Set(['--check', '--help', '--self-test']);
|
|
@@ -117,60 +126,158 @@ function plan(args) {
|
|
|
117
126
|
|
|
118
127
|
const manifest = readJson(path.join(pkgRoot, 'dist', 'outputs.json'), 'outputs.json (is the package built?)');
|
|
119
128
|
const available = Object.keys(manifest.files ?? {});
|
|
129
|
+
const dirKeys = [...new Set(available.flatMap((k) => k.split('/').slice(0, -1).map((_, i, parts) => parts.slice(0, i + 1).join('/') + '/')))].sort();
|
|
130
|
+
const realRoot = fs.realpathSync(consumerRoot);
|
|
120
131
|
const steps = [];
|
|
132
|
+
const dirs = [];
|
|
121
133
|
const problems = [];
|
|
134
|
+
|
|
135
|
+
/** Resolve a consumer-relative spec to an absolute path inside the root, or record why not. */
|
|
136
|
+
const inside = (key, destSpec, asDir) => {
|
|
137
|
+
if (typeof destSpec !== 'string' || destSpec === '' || path.isAbsolute(destSpec)) {
|
|
138
|
+
problems.push(`destination for "${key}" must be a relative path inside the consumer, got ${JSON.stringify(destSpec)}`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const abs = path.resolve(consumerRoot, destSpec);
|
|
142
|
+
const rel = path.relative(consumerRoot, abs);
|
|
143
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
144
|
+
problems.push(`destination for "${key}" escapes the consumer root: ${destSpec}`);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (rel === '' && asDir) {
|
|
148
|
+
problems.push(`destination for "${key}" is the consumer root; give a sub-directory`);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return { abs, rel };
|
|
152
|
+
};
|
|
153
|
+
|
|
122
154
|
for (const [key, destSpec] of Object.entries(design.outputs)) {
|
|
155
|
+
if (key.endsWith('/')) {
|
|
156
|
+
const members = available.filter((k) => k.startsWith(key));
|
|
157
|
+
if (!members.length) {
|
|
158
|
+
problems.push(`${design.package} does not ship "${key}". Available outputs:\n ${[...dirKeys, ...available].join('\n ')}`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (typeof destSpec !== 'string' || !destSpec.endsWith('/')) {
|
|
162
|
+
problems.push(`destination for directory output "${key}" must end in "/", got ${JSON.stringify(destSpec)}`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const d = inside(key, destSpec, true);
|
|
166
|
+
if (!d) continue;
|
|
167
|
+
dirs.push({ key, dest: d.abs, rel: d.rel });
|
|
168
|
+
for (const m of members) {
|
|
169
|
+
const dest = path.join(d.abs, m.slice(key.length));
|
|
170
|
+
steps.push({ key: m, src: path.join(pkgRoot, 'dist', m), dest, rel: path.relative(consumerRoot, dest), dirKey: key });
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
123
174
|
if (!available.includes(key)) {
|
|
124
|
-
problems.push(`${design.package} does not ship "${key}". Available outputs:\n ${available.join('\n ')}`);
|
|
175
|
+
problems.push(`${design.package} does not ship "${key}". Available outputs:\n ${[...dirKeys, ...available].join('\n ')}`);
|
|
125
176
|
continue;
|
|
126
177
|
}
|
|
127
178
|
if (typeof destSpec !== 'string' || destSpec === '' || path.isAbsolute(destSpec)) {
|
|
128
179
|
problems.push(`destination for "${key}" must be a relative path inside the consumer, got ${JSON.stringify(destSpec)}`);
|
|
129
180
|
continue;
|
|
130
181
|
}
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
if (rel
|
|
134
|
-
problems.push(`destination for "${key}" escapes the consumer root: ${destSpec}`);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (rel === '' || (fs.existsSync(dest) && fs.statSync(dest).isDirectory())) {
|
|
182
|
+
const d = inside(key, destSpec.endsWith('/') ? path.join(destSpec, path.basename(key)) : destSpec, false);
|
|
183
|
+
if (!d) continue;
|
|
184
|
+
if (d.rel === '' || (fs.existsSync(d.abs) && fs.statSync(d.abs).isDirectory())) {
|
|
138
185
|
problems.push(`destination for "${key}" is a directory: write "${destSpec.replace(/\/?$/, '/')}" to put the file inside it`);
|
|
139
186
|
continue;
|
|
140
187
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
188
|
+
steps.push({ key, src: path.join(pkgRoot, 'dist', key), dest: d.abs, rel: d.rel });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const exists = (p) => { try { fs.lstatSync(p); return true; } catch { return false; } };
|
|
192
|
+
// A directory output owns its destination.
|
|
193
|
+
const within = (child, parent) => child === parent || child.startsWith(parent + path.sep);
|
|
194
|
+
for (let i = 0; i < dirs.length; i++) {
|
|
195
|
+
for (let j = i + 1; j < dirs.length; j++) {
|
|
196
|
+
if (within(dirs[i].dest, dirs[j].dest) || within(dirs[j].dest, dirs[i].dest)) {
|
|
197
|
+
problems.push(`directory outputs "${dirs[i].key}" and "${dirs[j].key}" share or nest destinations (${dirs[i].rel}, ${dirs[j].rel}); a directory owns its destination`);
|
|
198
|
+
}
|
|
144
199
|
}
|
|
145
|
-
|
|
200
|
+
}
|
|
201
|
+
for (const s of steps) {
|
|
202
|
+
if (s.dirKey) continue;
|
|
203
|
+
const owner = dirs.find((d) => within(s.dest, d.dest));
|
|
204
|
+
if (owner) problems.push(`"${s.key}" is mapped into ${s.rel}, which directory output "${owner.key}" owns`);
|
|
205
|
+
}
|
|
206
|
+
for (const s of steps) {
|
|
207
|
+
if (s.dirKey && exists(s.dest) && fs.statSync(s.dest).isDirectory()) {
|
|
208
|
+
problems.push(`"${s.key}" would be written to ${s.rel}, which is an existing directory`);
|
|
209
|
+
}
|
|
210
|
+
const blocked = steps.find((o) => o !== s && within(o.dest, s.dest));
|
|
211
|
+
if (blocked) problems.push(`"${s.key}" is mapped to ${s.rel}, which is a parent directory of ${blocked.rel}`);
|
|
212
|
+
}
|
|
213
|
+
const seen = new Set();
|
|
214
|
+
for (const s of steps) {
|
|
215
|
+
if (seen.has(s.dest)) problems.push(`two outputs are mapped to the same destination: ${s.rel}`);
|
|
216
|
+
seen.add(s.dest);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Resolve symlinks: the nearest existing ancestor of every destination must stay inside the root.
|
|
220
|
+
const escapes = (dest) => {
|
|
221
|
+
let at = dest;
|
|
222
|
+
while (!exists(at)) at = path.dirname(at);
|
|
223
|
+
let real;
|
|
224
|
+
try { real = fs.realpathSync(at); } catch { return true; }
|
|
225
|
+
return !within(real, realRoot);
|
|
226
|
+
};
|
|
227
|
+
for (const target of [...dirs.map((d) => ({ key: d.key, dest: d.dest })), ...steps]) {
|
|
228
|
+
if (escapes(target.dest)) problems.push(`destination for "${target.key}" resolves outside the consumer root through a symlink`);
|
|
146
229
|
}
|
|
147
230
|
if (problems.length) throw new ConfigError(problems.join('\n '));
|
|
148
|
-
|
|
231
|
+
|
|
232
|
+
// Files under a directory destination that the release does not list (symlinks are never followed).
|
|
233
|
+
const listed = new Set(steps.map((s) => s.dest));
|
|
234
|
+
const extras = [];
|
|
235
|
+
const walk = (dir) => {
|
|
236
|
+
let entries;
|
|
237
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
238
|
+
for (const e of entries) {
|
|
239
|
+
const p = path.join(dir, e.name);
|
|
240
|
+
if (e.isSymbolicLink()) extras.push(path.relative(consumerRoot, p));
|
|
241
|
+
else if (e.isDirectory()) walk(p);
|
|
242
|
+
else if (!listed.has(p)) extras.push(path.relative(consumerRoot, p));
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
for (const d of dirs) if (exists(d.dest)) walk(d.dest);
|
|
246
|
+
return { steps, design, local, extras };
|
|
149
247
|
}
|
|
150
248
|
|
|
151
249
|
function sync(args) {
|
|
152
|
-
const { steps, design, local } = plan(args);
|
|
250
|
+
const { steps, design, local, extras } = plan(args);
|
|
153
251
|
for (const { key, src, dest, rel } of steps) {
|
|
154
252
|
let bytes = fs.readFileSync(src);
|
|
155
|
-
if (local) bytes = Buffer.from(localize(bytes.toString('utf8'), key, design.package, local.version, local.sha));
|
|
253
|
+
if (local && isText(key)) bytes = Buffer.from(localize(bytes.toString('utf8'), key, design.package, local.version, local.sha));
|
|
156
254
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
157
|
-
|
|
255
|
+
const tmp = `${dest}.dataimago-sync-tmp-${process.pid}`;
|
|
256
|
+
try {
|
|
257
|
+
fs.writeFileSync(tmp, bytes);
|
|
258
|
+
fs.renameSync(tmp, dest);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
fs.rmSync(tmp, { force: true });
|
|
261
|
+
throw e;
|
|
262
|
+
}
|
|
158
263
|
console.log(` ✓ ${rel} ← ${design.package}@${local ? `${LOCAL}(${local.sha})` : design.version} dist/${key}`);
|
|
159
264
|
}
|
|
265
|
+
for (const rel of extras) console.warn(` ! ${rel}: not in ${design.package}@${local ? `${LOCAL}(${local.sha})` : design.version}; sync never deletes, remove it`);
|
|
160
266
|
if (local) console.log(`\n⚠ local, unreleased design files: \`dataimago-sync --check\` fails until you re-sync from a release.`);
|
|
161
267
|
return 0;
|
|
162
268
|
}
|
|
163
269
|
|
|
164
270
|
function check(args) {
|
|
165
|
-
const { steps, design } = plan(args.filter((a) => a !== '--check'));
|
|
271
|
+
const { steps, design, extras } = plan(args.filter((a) => a !== '--check'));
|
|
166
272
|
const failures = [];
|
|
167
|
-
for (const { src, dest, rel } of steps) {
|
|
273
|
+
for (const { key, src, dest, rel } of steps) {
|
|
168
274
|
if (!fs.existsSync(dest)) { failures.push(`${rel}: missing`); continue; }
|
|
169
275
|
const have = fs.readFileSync(dest);
|
|
170
276
|
// The marker lives in the stamp: line 1, or the leading _meta object of a JSON file.
|
|
171
|
-
if (have.subarray(0, 1024).includes(LOCAL)) { failures.push(`${rel}: written by --from (local, unreleased); re-sync from a release`); continue; }
|
|
277
|
+
if (isText(key) && have.subarray(0, 1024).includes(LOCAL)) { failures.push(`${rel}: written by --from (local, unreleased); re-sync from a release`); continue; }
|
|
172
278
|
if (!have.equals(fs.readFileSync(src))) failures.push(`${rel}: differs from ${design.package}@${design.version}`);
|
|
173
279
|
}
|
|
280
|
+
for (const rel of extras) failures.push(`${rel}: not in ${design.package}@${design.version}; sync never deletes, remove it`);
|
|
174
281
|
for (const f of failures) console.error(` ✗ ${f}`);
|
|
175
282
|
if (failures.length) {
|
|
176
283
|
console.error(`\n✗ dataimago-sync --check: ${failures.length} file(s) out of sync. Run: npx -p ${design.package}@${design.version} dataimago-sync`);
|
|
@@ -211,6 +318,7 @@ async function selfTest() {
|
|
|
211
318
|
}
|
|
212
319
|
const { stamp, stampJson } = await import(stampUrl);
|
|
213
320
|
const assert = (await import('node:assert/strict')).default;
|
|
321
|
+
const { randomBytes } = await import('node:crypto');
|
|
214
322
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'di-sync-'));
|
|
215
323
|
|
|
216
324
|
// A fake published package: <tmp>/pkg with dist/{quarto/a.scss, dataviz/p.json, outputs.json, bin/}.
|
|
@@ -218,6 +326,9 @@ async function selfTest() {
|
|
|
218
326
|
const files = {
|
|
219
327
|
'quarto/a.scss': stamp({ kind: 'scss', pkg: name, version, distPath: 'quarto/a.scss', body: '$x: 1 !default;\n' }),
|
|
220
328
|
'dataviz/p.json': stampJson({ pkg: name, version, distPath: 'dataviz/p.json', data: { a: '#E69F00' } }),
|
|
329
|
+
'fonts/latin/fonts.css': stamp({ kind: 'css', pkg: name, version, distPath: 'fonts/latin/fonts.css', body: '@font-face { font-family: "Noto Sans"; src: url(x.woff2) format("woff2"); }\n' }),
|
|
330
|
+
'fonts/latin/x.woff2': Buffer.concat([Buffer.from([0xff, 0x00, 0xfe, 0x80]), randomBytes(28)]),
|
|
331
|
+
'fonts/latin/OFL.txt': 'SIL Open Font License 1.1\n',
|
|
221
332
|
};
|
|
222
333
|
fs.mkdirSync(path.join(root, 'dist', 'bin'), { recursive: true });
|
|
223
334
|
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name, version }));
|
|
@@ -334,6 +445,157 @@ async function selfTest() {
|
|
|
334
445
|
assert.equal(r.status, 2);
|
|
335
446
|
assert.ok(!exists('same.txt'));
|
|
336
447
|
|
|
448
|
+
// TEXT_EXT must stay inside what build-outputs' kindOf stamps, and binaries must stay outside it.
|
|
449
|
+
const { kindOf } = await import(new URL('./build-outputs.mjs', import.meta.url));
|
|
450
|
+
// True iff: an extension is in TEXT_EXT exactly when kindOf stamps it (non-binary).
|
|
451
|
+
const { KIND_BY_EXT, BINARY_EXT } = await import(new URL('./build-outputs.mjs', import.meta.url));
|
|
452
|
+
const kindFor = (ext) => kindOf(ext === '.template' ? 'x.md.template' : `x${ext}`);
|
|
453
|
+
for (const ext of TEXT_EXT) {
|
|
454
|
+
if (ext === '.template') continue; // an alias for the inner extension, not a kind of its own
|
|
455
|
+
assert.ok(ext in KIND_BY_EXT, `TEXT_EXT ${ext} maps to a stamp kind`);
|
|
456
|
+
assert.notEqual(kindFor(ext), 'binary', `kindOf stamps ${ext}`);
|
|
457
|
+
}
|
|
458
|
+
assert.ok(TEXT_EXT.includes('.template'), '.template is a text extension');
|
|
459
|
+
for (const ext of Object.keys(KIND_BY_EXT)) assert.ok(TEXT_EXT.includes(ext), `${ext} is stamped by kindOf, so TEXT_EXT must list it`);
|
|
460
|
+
for (const ext of BINARY_EXT) {
|
|
461
|
+
assert.ok(!TEXT_EXT.includes(ext), `${ext} is binary for sync`);
|
|
462
|
+
assert.equal(kindFor(ext), 'binary', `kindOf calls ${ext} binary`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Directory-output helpers: a clean consumer with the given outputs.
|
|
466
|
+
const fontsOut = { 'fonts/latin/': 'assets/fonts/latin/' };
|
|
467
|
+
const fresh = (outputs) => {
|
|
468
|
+
fs.rmSync(consumer, { recursive: true, force: true });
|
|
469
|
+
fs.mkdirSync(consumer, { recursive: true });
|
|
470
|
+
writeDesign({ ...base, outputs });
|
|
471
|
+
};
|
|
472
|
+
const bytes = (rel) => fs.readFileSync(path.join(consumer, rel));
|
|
473
|
+
const distBytes = (root, key) => fs.readFileSync(path.join(root, 'dist', key));
|
|
474
|
+
|
|
475
|
+
// (l) a directory output copies every listed file, byte-identical.
|
|
476
|
+
fresh(fontsOut);
|
|
477
|
+
r = run();
|
|
478
|
+
assert.equal(r.status, 0, r.stderr);
|
|
479
|
+
for (const f of ['fonts.css', 'x.woff2', 'OFL.txt']) {
|
|
480
|
+
assert.deepEqual(bytes(`assets/fonts/latin/${f}`), distBytes(path.join(tmp, 'pkg'), `fonts/latin/${f}`), f);
|
|
481
|
+
}
|
|
482
|
+
assert.equal(run('--check').status, 0);
|
|
483
|
+
// A directory key nothing lives under is "not shipped" and lists directory keys too.
|
|
484
|
+
fresh({ 'fonts/greek/': 'assets/fonts/greek/' });
|
|
485
|
+
r = run();
|
|
486
|
+
assert.equal(r.status, 2);
|
|
487
|
+
assert.match(r.stderr, /fonts\/greek\//);
|
|
488
|
+
assert.match(r.stderr, /fonts\/latin\//);
|
|
489
|
+
|
|
490
|
+
// (m) a directory output's destination must end in /.
|
|
491
|
+
fresh({ 'fonts/latin/': 'assets/fonts' });
|
|
492
|
+
r = run();
|
|
493
|
+
assert.equal(r.status, 2);
|
|
494
|
+
assert.ok(!exists('assets'), 'nothing written');
|
|
495
|
+
|
|
496
|
+
// (n) a directory owns its destination.
|
|
497
|
+
fresh({ 'fonts/latin/': 'assets/fonts/latin/', 'dataviz/p.json': 'assets/fonts/latin/p.json' });
|
|
498
|
+
r = run();
|
|
499
|
+
assert.equal(r.status, 2);
|
|
500
|
+
assert.ok(!exists('assets'), 'nothing written when a file lands in a directory output');
|
|
501
|
+
fresh({ 'quarto/': 'assets/fonts/latin/', 'fonts/latin/': 'assets/fonts/' });
|
|
502
|
+
r = run();
|
|
503
|
+
assert.equal(r.status, 2);
|
|
504
|
+
assert.ok(!exists('assets'), 'nothing written when directory outputs nest');
|
|
505
|
+
|
|
506
|
+
// (n2) a file output at a directory destination's ancestor, and a member landing on a directory.
|
|
507
|
+
fresh({ 'fonts/latin/x.woff2': 'assets/f', 'fonts/latin/': 'assets/f/latin/' });
|
|
508
|
+
r = run();
|
|
509
|
+
assert.equal(r.status, 2, r.stderr);
|
|
510
|
+
assert.ok(!exists('assets'), 'nothing written when a file is an ancestor of a directory destination');
|
|
511
|
+
fresh(fontsOut);
|
|
512
|
+
fs.mkdirSync(path.join(consumer, 'assets/fonts/latin/x.woff2'), { recursive: true });
|
|
513
|
+
r = run();
|
|
514
|
+
assert.equal(r.status, 2, r.stderr);
|
|
515
|
+
assert.ok(!exists('assets/fonts/latin/fonts.css'), 'nothing written when a member lands on a directory');
|
|
516
|
+
|
|
517
|
+
// (o) a stale file is named and kept: sync warns, --check fails.
|
|
518
|
+
fresh(fontsOut);
|
|
519
|
+
assert.equal(run().status, 0);
|
|
520
|
+
fs.writeFileSync(path.join(consumer, 'assets/fonts/latin/old.woff2'), 'stale');
|
|
521
|
+
r = run();
|
|
522
|
+
assert.equal(r.status, 0, r.stderr);
|
|
523
|
+
assert.match(r.stdout + r.stderr, /old\.woff2/);
|
|
524
|
+
assert.ok(exists('assets/fonts/latin/old.woff2'), 'sync never deletes');
|
|
525
|
+
r = run('--check');
|
|
526
|
+
assert.equal(r.status, 1);
|
|
527
|
+
assert.match(r.stderr, /old\.woff2/);
|
|
528
|
+
assert.match(r.stderr, /sync never deletes/);
|
|
529
|
+
// A symlink inside the destination is an extra too, and is left alone.
|
|
530
|
+
fs.rmSync(path.join(consumer, 'assets/fonts/latin/old.woff2'));
|
|
531
|
+
fs.symlinkSync(tmp, path.join(consumer, 'assets/fonts/latin/link'));
|
|
532
|
+
r = run('--check');
|
|
533
|
+
assert.equal(r.status, 1);
|
|
534
|
+
assert.match(r.stderr, /link/);
|
|
535
|
+
assert.equal(run().status, 0);
|
|
536
|
+
assert.ok(fs.lstatSync(path.join(consumer, 'assets/fonts/latin/link')).isSymbolicLink());
|
|
537
|
+
|
|
538
|
+
// (p) a byte edit and a deletion both fail --check.
|
|
539
|
+
fresh(fontsOut);
|
|
540
|
+
assert.equal(run().status, 0);
|
|
541
|
+
const woff = path.join(consumer, 'assets/fonts/latin/x.woff2');
|
|
542
|
+
const edited = fs.readFileSync(woff);
|
|
543
|
+
edited[5] ^= 0x01;
|
|
544
|
+
fs.writeFileSync(woff, edited);
|
|
545
|
+
r = run('--check');
|
|
546
|
+
assert.equal(r.status, 1);
|
|
547
|
+
assert.match(r.stderr, /x\.woff2/);
|
|
548
|
+
assert.equal(run().status, 0);
|
|
549
|
+
fs.rmSync(woff);
|
|
550
|
+
r = run('--check');
|
|
551
|
+
assert.equal(r.status, 1);
|
|
552
|
+
assert.match(r.stderr, /x\.woff2/);
|
|
553
|
+
|
|
554
|
+
// (q) --from leaves binaries byte-identical and localizes only stamped text.
|
|
555
|
+
fresh(fontsOut);
|
|
556
|
+
r = run('--from', path.join(tmp, 'checkout'));
|
|
557
|
+
assert.equal(r.status, 0, r.stderr);
|
|
558
|
+
const co = path.join(tmp, 'checkout', 'packages', 'css');
|
|
559
|
+
assert.deepEqual(bytes('assets/fonts/latin/x.woff2'), distBytes(co, 'fonts/latin/x.woff2'));
|
|
560
|
+
assert.deepEqual(bytes('assets/fonts/latin/OFL.txt'), distBytes(co, 'fonts/latin/OFL.txt'));
|
|
561
|
+
assert.match(bytes('assets/fonts/latin/fonts.css').toString('utf8').split('\n')[0], new RegExp(LOCAL));
|
|
562
|
+
assert.equal(run('--check').status, 1);
|
|
563
|
+
|
|
564
|
+
// (r) a symlink that resolves outside the consumer root is refused, for directory and file outputs.
|
|
565
|
+
const outside = path.join(tmp, 'outside');
|
|
566
|
+
fs.mkdirSync(outside, { recursive: true });
|
|
567
|
+
fresh(fontsOut);
|
|
568
|
+
fs.symlinkSync(outside, path.join(consumer, 'assets'));
|
|
569
|
+
r = run();
|
|
570
|
+
assert.equal(r.status, 2);
|
|
571
|
+
assert.deepEqual(fs.readdirSync(outside), [], 'nothing written outside the root');
|
|
572
|
+
fresh({ 'dataviz/p.json': 'assets/p.json' });
|
|
573
|
+
fs.symlinkSync(outside, path.join(consumer, 'assets'));
|
|
574
|
+
r = run();
|
|
575
|
+
assert.equal(r.status, 2);
|
|
576
|
+
assert.deepEqual(fs.readdirSync(outside), [], 'nothing written outside the root via a file output');
|
|
577
|
+
|
|
578
|
+
// (r2) a symlink under a directory output's destination that leaves the root is refused, writing nothing.
|
|
579
|
+
fresh({ 'fonts/': 'assets/fonts/' });
|
|
580
|
+
fs.mkdirSync(path.join(consumer, 'assets/fonts'), { recursive: true });
|
|
581
|
+
fs.symlinkSync(outside, path.join(consumer, 'assets/fonts/latin'));
|
|
582
|
+
r = run();
|
|
583
|
+
assert.equal(r.status, 2, r.stderr);
|
|
584
|
+
assert.deepEqual(fs.readdirSync(outside), [], 'nothing written outside the root via a member');
|
|
585
|
+
assert.ok(!exists('assets/fonts/dataviz'), 'nothing written at all');
|
|
586
|
+
|
|
587
|
+
// (s) writes are atomic per file: no temp file survives a sync.
|
|
588
|
+
fresh(fontsOut);
|
|
589
|
+
assert.equal(run().status, 0);
|
|
590
|
+
const leftovers = [];
|
|
591
|
+
(function walk(d) {
|
|
592
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
593
|
+
if (e.isDirectory()) walk(path.join(d, e.name));
|
|
594
|
+
else if (e.name.includes('.dataimago-sync-tmp')) leftovers.push(e.name);
|
|
595
|
+
}
|
|
596
|
+
})(consumer);
|
|
597
|
+
assert.deepEqual(leftovers, []);
|
|
598
|
+
|
|
337
599
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
338
600
|
console.log('dataimago-sync self-test: all checks passed');
|
|
339
601
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* @dataimago/css-neutral@0.1.0-alpha.
|
|
1
|
+
/* @dataimago/css-neutral@0.1.0-alpha.13 dist/dataviz/dataimago-dataviz.css · sha256:4fb36e2401c1cd21f57d50c017ac080377c3943362a5b6c5ce2cd658080e4499 · generated from tokens; do not edit · restore: npx -p @dataimago/css-neutral@0.1.0-alpha.13 dataimago-sync */
|
|
2
2
|
/* dataimago data-viz palette (Okabe-Ito). Categorical colour is never the only channel. */
|
|
3
3
|
:root, [data-bs-theme="light"], body.quarto-light {
|
|
4
4
|
--color-dataviz-okabe-ito-orange: #E69F00;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_meta": {
|
|
3
3
|
"package": "@dataimago/css-neutral",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.13",
|
|
5
5
|
"path": "dist/dataviz/dataimago-dataviz.json",
|
|
6
6
|
"sha256": "28160e151fca196be3e36790d6b67db232d8df060e8d9556b29dfee50f4b278b",
|
|
7
7
|
"note": "generated from tokens; do not edit",
|
|
8
|
-
"restore": "npx -p @dataimago/css-neutral@0.1.0-alpha.
|
|
8
|
+
"restore": "npx -p @dataimago/css-neutral@0.1.0-alpha.13 dataimago-sync"
|
|
9
9
|
},
|
|
10
10
|
"okabe-ito": {
|
|
11
11
|
"orange": "#E69F00",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
% @dataimago/css-neutral@0.1.0-alpha.
|
|
1
|
+
% @dataimago/css-neutral@0.1.0-alpha.13 dist/dataviz/dataimago-dataviz.tex · sha256:d7be36cb429ce8a1f19c95a7da8f57a7fcfda85fd2b0113e086913d37e9fa645 · generated from tokens; do not edit · restore: npx -p @dataimago/css-neutral@0.1.0-alpha.13 dataimago-sync
|
|
2
2
|
% dataimago data-viz palette (Okabe-Ito), light mode, for PSTricks and TikZ. Requires xcolor.
|
|
3
3
|
\definecolor{di-dataviz-okabe-ito-orange}{HTML}{E69F00}
|
|
4
4
|
\definecolor{di-dataviz-okabe-ito-sky-blue}{HTML}{56B4E9}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @dataimago/css-neutral@0.1.0-alpha.
|
|
1
|
+
# @dataimago/css-neutral@0.1.0-alpha.13 dist/dataviz/dataimago_dataviz.R · sha256:6443982f105b0f5e4a9d90ac2d1f6ce9fce2db4e8e8148b47f71bfc438940055 · generated from tokens; do not edit · restore: npx -p @dataimago/css-neutral@0.1.0-alpha.13 dataimago-sync
|
|
2
2
|
# dataimago data-viz palette (Okabe-Ito). Categorical colour is never the only channel:
|
|
3
3
|
# pair it with marker shape, line type or a label.
|
|
4
4
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|