openvibers/openvibe.shared Commits

scripts/pull-glyph-drafts.js: approved drafts into the registry, with their solids (#236)

OP OpenVibers committed 12 days ago · d978dc47e3 · 2 files, +156 −0

Reads the public list of approved drafts (Design's /api/v1/glyphs/drafts), checks each drawing again shape by shape,
and with --apply writes a new name's line drawing into DRAWN and its solid twin, when it passes, into SOLID, each with
where it came from. A name the set has, a failing drawing and a second draft for one name are left out; without
--apply nothing is written.

Changes

ddiff --git a/scripts/pull-glyph-drafts.js b/scripts/pull-glyph-drafts.js
nnew file mode 100644
iindex 0000000..65600e1
---- /dev/null
++++ b/scripts/pull-glyph-drafts.js
@@@ -0,0 +1,105 @@
++#!/usr/bin/env node
++'use strict';
++// Takes the icon drafts approved on openvibe.design into the registry (plan L50): each approved draft whose name the
++// set does not have yet becomes a line drawing in glyphs.js (DRAWN), and its solid twin, when it has one, a SOLID
++// entry. Every drawing is checked again here, shape by shape, before it is written: a draft that fails is reported and
++// left out. Nothing is fetched but the public list of approved drafts.
++//
++// node scripts/pull-glyph-drafts.js what would be added (dry run)
++// node scripts/pull-glyph-drafts.js --apply write glyphs.js
++// node scripts/pull-glyph-drafts.js --from <url|file> another Design, or a saved list (the tests)
++//
++// Then: node scripts/build-glyphs.js, node scripts/build-navbar-icons.js, node scripts/sibling-revs.js, a CHANGELOG line,
++// a minor release. A Font Awesome name for the new glyph, if any, is added to FA by hand.
++const fs = require('fs');
++const path = require('path');
++
++// PULL_GLYPHS_FILE: write a copy instead (the tests).
++const FILE = process.env.PULL_GLYPHS_FILE || path.join(__dirname, '..', 'glyphs.js');
++const argv = process.argv.slice(2);
++const APPLY = argv.includes('--apply');
++const FROM = (() => { const i = argv.indexOf('--from'); return i >= 0 ? argv[i + 1] : 'https://openvibe.design'; })();
++
++const SHAPES = { path: ['d'], circle: ['cx', 'cy', 'r'], ellipse: ['cx', 'cy', 'rx', 'ry'], rect: ['x', 'y', 'width', 'height', 'rx', 'ry'], line: ['x1', 'y1', 'x2', 'y2'], polyline: ['points'] };
++const NUM = /^-?\d*\.?\d+$/;
++const PATH_D = /^[MmLlHhVvCcSsQqTtAaZz0-9.,\s-]+$/;
++
++/**
++ * A drawing as this file stores it, or null: shapes only, the known attributes with numbers (a path's d is commands
++ * and numbers), and for a solid also fill-rule="evenodd" on a path and class="ln". At most 8 shapes, on the grid.
++ */
++function check(markup, solid) {
++ let rest = String(markup || '').trim();
++ if (!rest || rest.length > 3000) return null;
++ const out = [];
++ const tag = /^<\s*(\w+)((?:\s+[\w:-]+\s*=\s*"[^"]*")*)\s*\/>\s*/;
++ while (rest.length) {
++ const m = tag.exec(rest);
++ if (!m || !SHAPES[m[1]]) return null;
++ const attrs = {};
++ for (const a of m[2].matchAll(/([\w:-]+)\s*=\s*"([^"]*)"/g)) attrs[a[1]] = a[2];
++ for (const [k, v] of Object.entries(attrs)) {
++ if (SHAPES[m[1]].includes(k)) {
++ if (k === 'd' ? !PATH_D.test(v) || !/^[Mm]/.test(v) : k === 'points' ? !/^[\d.,\s-]+$/.test(v) : !NUM.test(v)) return null;
++ } else if (solid && k === 'fill-rule' && m[1] === 'path' && v === 'evenodd') {
++ // a hole
++ } else if (solid && k === 'class' && v === 'ln') {
++ // a part that stays a line
++ } else if (!solid && k === 'stroke-width' && NUM.test(v) && +v >= 1 && +v <= 4) {
++ // a heavier dot
++ } else return null;
++ }
++ if ((String(m[2]).match(/-?\d*\.?\d+/g) || []).map(Number).some((n) => n < -25 || n > 50)) return null;
++ out.push(m[0].trim());
++ if (out.length > 8) return null;
++ rest = rest.slice(m[0].length);
++ }
++ return out.join('');
++}
++
++async function drafts() {
++ if (/^https?:\/\//.test(FROM)) {
++ const r = await fetch(`${FROM.replace(/\/+$/, '')}/api/v1/glyphs/drafts?status=approved`, { headers: { accept: 'application/json' } });
++ if (!r.ok) throw new Error(`${FROM} answered ${r.status}`);
++ return (await r.json()).drafts || [];
++ }
++ return JSON.parse(fs.readFileSync(FROM, 'utf8')).drafts || [];
++}
++
++(async () => {
++ const glyphs = require('../glyphs');
++ const have = new Set(Array.isArray(glyphs.NAMES) ? glyphs.NAMES : Object.keys(glyphs.NAMES));
++ const list = await drafts();
++ const add = [];
++ const seen = new Set();
++ for (const d of list) {
++ const name = String(d.name || '');
++ if (!/^[a-z][a-z0-9-]{1,31}$/.test(name)) { console.log(`skip ${JSON.stringify(name)}: not a glyph name`); continue; }
++ if (have.has(name) || seen.has(name)) { console.log(`skip ${name}: the set has it (or an earlier draft took the name)`); continue; }
++ const line = check(d.svg, false);
++ if (!line) { console.log(`skip ${name} (${d.id}): its drawing fails the checks`); continue; }
++ const solid = d.solid_svg ? check(d.solid_svg, true) : null;
++ if (d.solid_svg && !solid) console.log(`note ${name}: its solid twin fails the checks; the line drawing is taken alone`);
++ seen.add(name);
++ add.push({ name, id: d.id, line, solid, sentence: String(d.sentence || '').replace(/\s+/g, ' ').slice(0, 120) });
++ }
++ if (!add.length) { console.log(`nothing to add (${list.length} approved draft${list.length === 1 ? '' : 's'})`); return; }
++ for (const a of add) console.log(`${APPLY ? 'add' : 'would add'} ${a.name}${a.solid ? ' (and its solid)' : ''}: ${a.sentence || 'no sentence'}`);
++ if (!APPLY) { console.log('\nrun with --apply to write glyphs.js'); return; }
++
++ let src = fs.readFileSync(FILE, 'utf8');
++ const key = (n) => (/^[a-z]+$/.test(n) ? n : `'${n}'`);
++ const day = new Date().toISOString().slice(0, 10);
++ const lines = add.map((a) => ` // ${day}: drafted on openvibe.design (${a.id}) and approved${a.sentence ? `: ${a.sentence.replace(/\*\//g, '')}` : ''}.\n ${key(a.name)}: '${a.line}',`).join('\n');
++ const drawnEnd = src.indexOf('\n };\n', src.indexOf(' const DRAWN = {'));
++ if (drawnEnd < 0) throw new Error('glyphs.js: the end of DRAWN was not found');
++ src = `${src.slice(0, drawnEnd)}\n${lines}${src.slice(drawnEnd)}`;
++ const solids = add.filter((a) => a.solid).map((a) => ` ${key(a.name)}: '${a.solid}',`).join('\n');
++ if (solids) {
++ const solidEnd = src.indexOf('\n });\n', src.indexOf(' const SOLID = Object.freeze({'));
++ if (solidEnd < 0) throw new Error('glyphs.js: the end of SOLID was not found');
++ src = `${src.slice(0, solidEnd)}\n // ${day}: the solid twins of the drafts taken above.\n${solids}${src.slice(solidEnd)}`;
++ }
++ fs.writeFileSync(FILE, src);
++ console.log('\nglyphs.js written. Now: node scripts/build-glyphs.js && node scripts/build-navbar-icons.js && node scripts/sibling-revs.js');
++})().catch((err) => { console.error(`pull-glyph-drafts: ${err.message}`); process.exit(1); });
ddiff --git a/test/pull-glyph-drafts.test.js b/test/pull-glyph-drafts.test.js
nnew file mode 100644
iindex 0000000..c8a1de0
---- /dev/null
++++ b/test/pull-glyph-drafts.test.js
@@@ -0,0 +1,51 @@
++'use strict';
++/**
++ * scripts/pull-glyph-drafts.js (plan L50): approved drafts from openvibe.design become line drawings in DRAWN and, with
++ * a solid twin, SOLID entries; a name the set has, a drawing that fails the checks (a script, a style, a colour) and
++ * a second draft for the same name are left out; without --apply nothing is written. The run here reads a saved list
++ * and writes a copy of glyphs.js (PULL_GLYPHS_FILE).
++ */
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const { execFileSync } = require('child_process');
++
++const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pull-drafts-'));
++const list = path.join(dir, 'drafts.json');
++const copy = path.join(dir, 'glyphs.js');
++const original = fs.readFileSync(path.join(__dirname, '..', 'glyphs.js'), 'utf8');
++fs.writeFileSync(copy, original);
++fs.writeFileSync(list, JSON.stringify({ drafts: [
++ { id: 'gd_1', name: 'sauna', sentence: 'A tub with steam over it', svg: '<path d="M4 12h16v6H4Z"/><path d="M9 4c0 2 2 2 2 4"/>', solid_svg: '<path d="M3.5 11.5h17v7h-17Z"/><path class="ln" d="M9 4c0 2 2 2 2 4"/>' },
++ { id: 'gd_2', name: 'kite', sentence: 'A kite', svg: '<path d="M12 3l6 8-6 10-6-10Z"/>', solid_svg: '<path d="M12 3l6 8-6 10-6-10Z" style="fill:red"/>' },
++ { id: 'gd_3', name: 'bell', sentence: 'Already in the set', svg: '<path d="M4 4h16"/>' },
++ { id: 'gd_4', name: 'evil', sentence: 'A script', svg: '<script>alert(1)</script>' },
++ { id: 'gd_5', name: 'kite', sentence: 'A second kite', svg: '<path d="M4 4h16"/>' },
++] }));
++
++const run = (...args) => execFileSync(process.execPath, [path.join(__dirname, '..', 'scripts', 'pull-glyph-drafts.js'), '--from', list, ...args], { encoding: 'utf8', env: { ...process.env, PULL_GLYPHS_FILE: copy } });
++try {
++ const dry = run();
++ assert.match(dry, /would add sauna \(and its solid\): A tub with steam over it/);
++ assert.match(dry, /would add kite: A kite/);
++ assert.match(dry, /note kite: its solid twin fails the checks/);
++ assert.match(dry, /skip bell: the set has it/);
++ assert.match(dry, /skip evil \(gd_4\): its drawing fails the checks/);
++ assert.match(dry, /skip kite: the set has it \(or an earlier draft took the name\)/);
++ assert.strictEqual(fs.readFileSync(copy, 'utf8'), original, 'a dry run writes nothing');
++
++ run('--apply');
++ const out = fs.readFileSync(copy, 'utf8');
++ const drawn = out.slice(out.indexOf(' const DRAWN = {'), out.indexOf(' const SOLID = Object.freeze({'));
++ const solid = out.slice(out.indexOf(' const SOLID = Object.freeze({'), out.indexOf('const BY = '));
++ assert.ok(drawn.includes(`sauna: '<path d="M4 12h16v6H4Z"/><path d="M9 4c0 2 2 2 2 4"/>',`), 'the line drawing is in DRAWN');
++ assert.ok(drawn.includes(`kite: '<path d="M12 3l6 8-6 10-6-10Z"/>',`));
++ assert.ok(drawn.includes('drafted on openvibe.design (gd_1) and approved: A tub with steam over it.'), 'with where it came from');
++ assert.ok(solid.includes(`sauna: '<path d="M3.5 11.5h17v7h-17Z"/><path class="ln" d="M9 4c0 2 2 2 2 4"/>',`), 'its solid twin is in SOLID');
++ assert.ok(!out.includes('fill:red') && !out.includes('<script'), 'nothing that failed is written');
++ execFileSync(process.execPath, ['--check', copy]);
++ console.log('pull-glyph-drafts: adds what passes, where it belongs, and nothing else');
++} finally {
++ fs.rmSync(dir, { recursive: true, force: true });
++}