Problem: the hexagonal refactor churns the backend tree for nine more phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper, compose/web image) must move to its own repo first so doc/layout rewrites land once on a backend-only tree. Change: - New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/ (updateURL repointed to oikos-web releases), compose/, own CI (web + desktop jobs), own deploy script (CI-green gate, TOCTOU guard, version-tagged images, prune-to-3), own webhook receiver on :9798 + launchd unit, own compose project publishing the same 8091:80. - Cutover executed on mac-mini in order: oikos stack's web service stopped+removed, oikos-web project brought up on 8091; outer Caddy untouched (targets the published port) — serving + Authentik flow + /wails 404 quirk verified post-cutover. - Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install targets, the compose web service, oikos-web from deploy.sh's fallback prune list; wails + go-keyring dropped from go.mod, vendor synced. - README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now point at the new repo; mbse + mascot design docs carry a path note. Risk: production SPA serving depends on the new pipeline now; rollback is versioned-image re-up of the old web service from a pre-split checkout (port 8091). Desktop builds installed before the split still check dtoro/oikos releases — one manual reinstall, noted in the oikos-web release notes. Verification: go vet, make test (race), make generate-check, golangci (no new findings; baseline down 400→365); post-cutover curls — localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
168 lines
5.3 KiB
JavaScript
168 lines
5.3 KiB
JavaScript
// Forked from https://github.com/jsdom/jsdom/blob/main/lib/jsdom/living/helpers/strings.js
|
|
|
|
"use strict";
|
|
|
|
// https://infra.spec.whatwg.org/#ascii-whitespace
|
|
const asciiWhitespaceRe = /^[\t\n\f\r ]$/;
|
|
exports.asciiWhitespaceRe = asciiWhitespaceRe;
|
|
|
|
// https://infra.spec.whatwg.org/#ascii-lowercase
|
|
exports.asciiLowercase = (s) => {
|
|
const len = s.length;
|
|
const out = new Array(len);
|
|
for (let i = 0; i < len; i++) {
|
|
const code = s.charCodeAt(i);
|
|
// If the character is between 'A' (65) and 'Z' (90), convert using bitwise OR with 32
|
|
out[i] = code >= 65 && code <= 90 ? String.fromCharCode(code | 32) : s[i];
|
|
}
|
|
return out.join("");
|
|
};
|
|
|
|
// https://infra.spec.whatwg.org/#ascii-uppercase
|
|
exports.asciiUppercase = (s) => {
|
|
const len = s.length;
|
|
const out = new Array(len);
|
|
for (let i = 0; i < len; i++) {
|
|
const code = s.charCodeAt(i);
|
|
// If the character is between 'a' (97) and 'z' (122), convert using bitwise AND with ~32
|
|
out[i] = code >= 97 && code <= 122 ? String.fromCharCode(code & ~32) : s[i];
|
|
}
|
|
return out.join("");
|
|
};
|
|
|
|
// https://infra.spec.whatwg.org/#strip-newlines
|
|
exports.stripNewlines = (s) => {
|
|
return s.replace(/[\n\r]+/g, "");
|
|
};
|
|
|
|
// https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace
|
|
exports.stripLeadingAndTrailingASCIIWhitespace = (s) => {
|
|
return s.replace(/^[ \t\n\f\r]+/, "").replace(/[ \t\n\f\r]+$/, "");
|
|
};
|
|
|
|
// https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace
|
|
exports.stripAndCollapseASCIIWhitespace = (s) => {
|
|
return s
|
|
.replace(/[ \t\n\f\r]+/g, " ")
|
|
.replace(/^[ \t\n\f\r]+/, "")
|
|
.replace(/[ \t\n\f\r]+$/, "");
|
|
};
|
|
|
|
// https://html.spec.whatwg.org/multipage/infrastructure.html#valid-simple-colour
|
|
exports.isValidSimpleColor = (s) => {
|
|
return /^#[a-fA-F\d]{6}$/.test(s);
|
|
};
|
|
|
|
// https://infra.spec.whatwg.org/#ascii-case-insensitive
|
|
exports.asciiCaseInsensitiveMatch = (a, b) => {
|
|
if (a.length !== b.length) {
|
|
return false;
|
|
}
|
|
|
|
for (let i = 0; i < a.length; ++i) {
|
|
if ((a.charCodeAt(i) | 32) !== (b.charCodeAt(i) | 32)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-integers
|
|
// Error is represented as null.
|
|
const parseInteger = (exports.parseInteger = (input) => {
|
|
// The implementation here is slightly different from the spec's. We want to use parseInt(), but parseInt() trims
|
|
// Unicode whitespace in addition to just ASCII ones, so we make sure that the trimmed prefix contains only ASCII
|
|
// whitespace ourselves.
|
|
const numWhitespace = input.length - input.trimStart().length;
|
|
if (/[^\t\n\f\r ]/.test(input.slice(0, numWhitespace))) {
|
|
return null;
|
|
}
|
|
// We don't allow hexadecimal numbers here.
|
|
// eslint-disable-next-line radix
|
|
const value = parseInt(input, 10);
|
|
if (Number.isNaN(value)) {
|
|
return null;
|
|
}
|
|
// parseInt() returns -0 for "-0". Normalize that here.
|
|
return value === 0 ? 0 : value;
|
|
});
|
|
|
|
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-non-negative-integers
|
|
// Error is represented as null.
|
|
exports.parseNonNegativeInteger = (input) => {
|
|
const value = parseInteger(input);
|
|
if (value === null) {
|
|
return null;
|
|
}
|
|
if (value < 0) {
|
|
return null;
|
|
}
|
|
return value;
|
|
};
|
|
|
|
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-floating-point-number
|
|
const floatingPointNumRe = /^-?(?:\d+|\d*\.\d+)(?:[eE][-+]?\d+)?$/;
|
|
exports.isValidFloatingPointNumber = (str) => floatingPointNumRe.test(str);
|
|
|
|
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-floating-point-number-values
|
|
// Error is represented as null.
|
|
exports.parseFloatingPointNumber = (str) => {
|
|
// The implementation here is slightly different from the spec's. We need to use parseFloat() in order to retain
|
|
// accuracy, but parseFloat() trims Unicode whitespace in addition to just ASCII ones, so we make sure that the
|
|
// trimmed prefix contains only ASCII whitespace ourselves.
|
|
const numWhitespace = str.length - str.trimStart().length;
|
|
if (/[^\t\n\f\r ]/.test(str.slice(0, numWhitespace))) {
|
|
return null;
|
|
}
|
|
const parsed = parseFloat(str);
|
|
return isFinite(parsed) ? parsed : null;
|
|
};
|
|
|
|
// https://infra.spec.whatwg.org/#split-on-ascii-whitespace
|
|
exports.splitOnASCIIWhitespace = (str) => {
|
|
let position = 0;
|
|
const tokens = [];
|
|
while (position < str.length && asciiWhitespaceRe.test(str[position])) {
|
|
position++;
|
|
}
|
|
if (position === str.length) {
|
|
return tokens;
|
|
}
|
|
while (position < str.length) {
|
|
const start = position;
|
|
while (position < str.length && !asciiWhitespaceRe.test(str[position])) {
|
|
position++;
|
|
}
|
|
tokens.push(str.slice(start, position));
|
|
while (position < str.length && asciiWhitespaceRe.test(str[position])) {
|
|
position++;
|
|
}
|
|
}
|
|
return tokens;
|
|
};
|
|
|
|
// https://infra.spec.whatwg.org/#split-on-commas
|
|
exports.splitOnCommas = (str) => {
|
|
let position = 0;
|
|
const tokens = [];
|
|
while (position < str.length) {
|
|
let start = position;
|
|
while (position < str.length && str[position] !== ",") {
|
|
position++;
|
|
}
|
|
let end = position;
|
|
while (start < str.length && asciiWhitespaceRe.test(str[start])) {
|
|
start++;
|
|
}
|
|
while (end > start && asciiWhitespaceRe.test(str[end - 1])) {
|
|
end--;
|
|
}
|
|
tokens.push(str.slice(start, end));
|
|
if (position < str.length) {
|
|
position++;
|
|
}
|
|
}
|
|
return tokens;
|
|
};
|