Skip to content

Commit 545189e

Browse files
forge: extract the helpers section to lib/helpers.js
Second split slice: the whole "// helpers" section — HTML esc, path/ref validators (okSegment/okPath/okRef), server-side formatters (relTime, fmtBytes, looksBinary, identicon, labelChips), and the GitHub-style SVG icon constants — lifts out as pure functions over node:crypto. REF_RE moves with it and is re-exported (plugin.js still validates branch names with it). Behavior identical — 127 tests still green. No redeploy.
1 parent 4ac8867 commit 545189e

2 files changed

Lines changed: 113 additions & 96 deletions

File tree

forge/lib/helpers.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// forge/lib/helpers.js — pure presentation & validation helpers extracted from
2+
// plugin.js's "// helpers" section: HTML escaping, path/ref validators, small
3+
// server-side formatters, and the GitHub-style SVG icon constants. No forge
4+
// state; only node:crypto (identicon). REF_RE is re-exported because plugin.js
5+
// also validates branch names with it.
6+
import crypto from 'node:crypto';
7+
8+
// Path/ref validation regexes (BAD_SEG stays internal; REF_RE is also used by
9+
// the caller for branch validation, so it's exported).
10+
export const REF_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/;
11+
const BAD_SEG = /^\.|\.\.|[\\\x00-\x1f]|%2e|%2f|%5c/i;
12+
13+
const ESC = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
14+
export const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ESC[c]);
15+
16+
export function okSegment(s) {
17+
return typeof s === 'string' && s.length > 0 && s.length <= 255 && !BAD_SEG.test(s);
18+
}
19+
export function okPath(segs) {
20+
return segs.every(okSegment);
21+
}
22+
export function okRef(ref) {
23+
return REF_RE.test(ref) && !ref.includes('..') && !ref.endsWith('/') && !ref.includes('//');
24+
}
25+
26+
/** Unix seconds -> "3 days ago" (server-side; no client JS needed). */
27+
export function relTime(unixSecs) {
28+
const s = Math.floor(Date.now() / 1000) - unixSecs;
29+
if (!Number.isFinite(s)) return '';
30+
if (s < 45) return 'just now';
31+
const steps = [[60, 'minute'], [3600, 'hour'], [86400, 'day'], [2592000, 'month'], [31536000, 'year']];
32+
for (let i = 0; i < steps.length; i++) {
33+
const next = steps[i + 1];
34+
if (!next || s < next[0]) {
35+
const n = Math.max(1, Math.round(s / steps[i][0]));
36+
return `${n} ${steps[i][1]}${n === 1 ? '' : 's'} ago`;
37+
}
38+
}
39+
return '';
40+
}
41+
42+
/**
43+
* GitHub-style chip readability: perceptual luminance of the (validated
44+
* 6-hex) label color decides black-ish or white text on the chip.
45+
*/
46+
export function labelTextColor(hex) {
47+
const r = parseInt(hex.slice(0, 2), 16);
48+
const g = parseInt(hex.slice(2, 4), 16);
49+
const b = parseInt(hex.slice(4, 6), 16);
50+
return (0.299 * r + 0.587 * g + 0.114 * b) > 140 ? '#1f2328' : '#ffffff';
51+
}
52+
53+
/** Rounded-full colored chips for resolved label defs [{name, color}]. */
54+
export function labelChips(labels) {
55+
return labels.map((l) => `<span class="label-chip" style="background:#${esc(l.color)};color:${labelTextColor(l.color)}">${esc(l.name)}</span>`).join(' ');
56+
}
57+
58+
export function fmtBytes(n) {
59+
if (!Number.isFinite(n)) return '';
60+
const units = ['B', 'KB', 'MB', 'GB'];
61+
let i = 0; let v = n;
62+
while (v >= 1024 && i < units.length - 1) { v /= 1024; i += 1; }
63+
return `${i ? v.toFixed(1) : v} ${units[i]}`;
64+
}
65+
66+
/** NUL in the first 8000 bytes => binary (git's own heuristic). */
67+
export function looksBinary(buf) {
68+
return buf.subarray(0, 8000).includes(0);
69+
}
70+
71+
/**
72+
* Deterministic GitHub-style identicon: 5x5 mirrored pixel SVG from
73+
* md5(email), hue from the trailing hash bytes, #f0f0f0 background.
74+
*/
75+
export function identicon(email, px = 20) {
76+
const hash = crypto.createHash('md5').update(String(email || '?').trim().toLowerCase()).digest();
77+
const hue = ((hash[12] << 8) | hash[13]) % 360;
78+
const fg = `hsl(${hue},46%,52%)`;
79+
let rects = '';
80+
for (let r = 0; r < 5; r++) {
81+
for (let c = 0; c < 3; c++) {
82+
if (hash[r * 3 + c] % 2 === 0) {
83+
rects += `<rect x="${c + 1}" y="${r + 1}" width="1" height="1"/>`;
84+
if (c < 2) rects += `<rect x="${5 - c}" y="${r + 1}" width="1" height="1"/>`;
85+
}
86+
}
87+
}
88+
return `<svg class="avatar" width="${px}" height="${px}" viewBox="0 0 7 7" shape-rendering="crispEdges" role="img" aria-label="identicon">`
89+
+ `<rect width="7" height="7" fill="#f0f0f0"/><g fill="${fg}">${rects}</g></svg>`;
90+
}
91+
92+
export const ICON_DIR = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="#54aeff" aria-hidden="true"><path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"/></svg>';
93+
export const ICON_FILE = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="#59636e" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/></svg>';
94+
export const ICON_REPO = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="#59636e" aria-hidden="true"><path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z"/></svg>';
95+
export const ICON_BRANCH = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/></svg>';
96+
export const ICON_TAG = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"/></svg>';
97+
// GitHub's issue-opened (green circle-dot) and issue-closed (purple check).
98+
export const ICON_ISSUE_OPEN = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="#1a7f37" aria-hidden="true"><path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"/><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"/></svg>';
99+
export const ICON_ISSUE_CLOSED = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="#8250df" aria-hidden="true"><path d="M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z"/><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z"/></svg>';
100+
// GitHub's git-pull-request (green open), git-merge (purple merged) and
101+
// git-pull-request-closed (red closed) octicons — tier 3a.
102+
const PR_PATH = 'M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z';
103+
export const ICON_PR_OPEN = `<svg class="icon pr-open" width="16" height="16" viewBox="0 0 16 16" fill="#1a7f37" aria-hidden="true"><path d="${PR_PATH}"/></svg>`;
104+
export const ICON_PR_TAB = `<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="${PR_PATH}"/></svg>`;
105+
export const ICON_PR_MERGED = '<svg class="icon pr-merged" width="16" height="16" viewBox="0 0 16 16" fill="#8250df" aria-hidden="true"><path d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z"/></svg>';
106+
export const ICON_PR_CLOSED = '<svg class="icon pr-closed" width="16" height="16" viewBox="0 0 16 16" fill="#cf222e" aria-hidden="true"><path d="M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75Zm-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-.97-.97-.97.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/></svg>';

forge/plugin.js

Lines changed: 7 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ import {
137137
} from './lib/blocktrails.js';
138138
// re-export the four the test suite imports from plugin.js (back-compat)
139139
export { markStateHash, npubEncode, trailAddress, trailProgram } from './lib/blocktrails.js';
140+
import {
141+
esc, okSegment, okPath, okRef, relTime, labelTextColor, labelChips, fmtBytes, looksBinary, identicon, REF_RE,
142+
ICON_DIR, ICON_FILE, ICON_REPO, ICON_BRANCH, ICON_TAG, ICON_ISSUE_OPEN, ICON_ISSUE_CLOSED,
143+
ICON_PR_OPEN, ICON_PR_TAB, ICON_PR_MERGED, ICON_PR_CLOSED,
144+
} from './lib/helpers.js';
140145

141146
const execFileP = promisify(execFile);
142147

@@ -149,11 +154,9 @@ const SERVICES = new Set(['git-upload-pack', 'git-receive-pack']);
149154
const OWNER_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
150155
const REPO_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
151156
// Refs may contain '/' (feature/x) but never '..'; shas are plain hex.
152-
const REF_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/;
153157
const SHA_RE = /^[0-9a-f]{4,64}$/;
154158
// Path segments: reject empties, traversal, backslash, control chars and
155159
// percent-encoded dot/slash/backslash (belt and braces if decoding varies).
156-
const BAD_SEG = /^\.|\.\.|[\\\x00-\x1f]|%2e|%2f|%5c/i;
157160

158161
// Web-edit (tier 3.7): a single repo-relative file path the browser edits.
159162
// One segment = a conservative filename charset (no spaces, no exotica —
@@ -224,100 +227,8 @@ const SATS_CAP = 2_100_000_000_000_000; // 21M BTC in sats
224227

225228
// ---------------------------------------------------------------- helpers
226229

227-
const ESC = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
228-
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ESC[c]);
229-
230-
function okSegment(s) {
231-
return typeof s === 'string' && s.length > 0 && s.length <= 255 && !BAD_SEG.test(s);
232-
}
233-
function okPath(segs) {
234-
return segs.every(okSegment);
235-
}
236-
function okRef(ref) {
237-
return REF_RE.test(ref) && !ref.includes('..') && !ref.endsWith('/') && !ref.includes('//');
238-
}
239-
240-
/** Unix seconds -> "3 days ago" (server-side; no client JS needed). */
241-
function relTime(unixSecs) {
242-
const s = Math.floor(Date.now() / 1000) - unixSecs;
243-
if (!Number.isFinite(s)) return '';
244-
if (s < 45) return 'just now';
245-
const steps = [[60, 'minute'], [3600, 'hour'], [86400, 'day'], [2592000, 'month'], [31536000, 'year']];
246-
for (let i = 0; i < steps.length; i++) {
247-
const next = steps[i + 1];
248-
if (!next || s < next[0]) {
249-
const n = Math.max(1, Math.round(s / steps[i][0]));
250-
return `${n} ${steps[i][1]}${n === 1 ? '' : 's'} ago`;
251-
}
252-
}
253-
return '';
254-
}
255-
256-
/**
257-
* GitHub-style chip readability: perceptual luminance of the (validated
258-
* 6-hex) label color decides black-ish or white text on the chip.
259-
*/
260-
function labelTextColor(hex) {
261-
const r = parseInt(hex.slice(0, 2), 16);
262-
const g = parseInt(hex.slice(2, 4), 16);
263-
const b = parseInt(hex.slice(4, 6), 16);
264-
return (0.299 * r + 0.587 * g + 0.114 * b) > 140 ? '#1f2328' : '#ffffff';
265-
}
266-
267-
/** Rounded-full colored chips for resolved label defs [{name, color}]. */
268-
function labelChips(labels) {
269-
return labels.map((l) => `<span class="label-chip" style="background:#${esc(l.color)};color:${labelTextColor(l.color)}">${esc(l.name)}</span>`).join(' ');
270-
}
271-
272-
function fmtBytes(n) {
273-
if (!Number.isFinite(n)) return '';
274-
const units = ['B', 'KB', 'MB', 'GB'];
275-
let i = 0; let v = n;
276-
while (v >= 1024 && i < units.length - 1) { v /= 1024; i += 1; }
277-
return `${i ? v.toFixed(1) : v} ${units[i]}`;
278-
}
279-
280-
/** NUL in the first 8000 bytes => binary (git's own heuristic). */
281-
function looksBinary(buf) {
282-
return buf.subarray(0, 8000).includes(0);
283-
}
284-
285-
/**
286-
* Deterministic GitHub-style identicon: 5x5 mirrored pixel SVG from
287-
* md5(email), hue from the trailing hash bytes, #f0f0f0 background.
288-
*/
289-
function identicon(email, px = 20) {
290-
const hash = crypto.createHash('md5').update(String(email || '?').trim().toLowerCase()).digest();
291-
const hue = ((hash[12] << 8) | hash[13]) % 360;
292-
const fg = `hsl(${hue},46%,52%)`;
293-
let rects = '';
294-
for (let r = 0; r < 5; r++) {
295-
for (let c = 0; c < 3; c++) {
296-
if (hash[r * 3 + c] % 2 === 0) {
297-
rects += `<rect x="${c + 1}" y="${r + 1}" width="1" height="1"/>`;
298-
if (c < 2) rects += `<rect x="${5 - c}" y="${r + 1}" width="1" height="1"/>`;
299-
}
300-
}
301-
}
302-
return `<svg class="avatar" width="${px}" height="${px}" viewBox="0 0 7 7" shape-rendering="crispEdges" role="img" aria-label="identicon">`
303-
+ `<rect width="7" height="7" fill="#f0f0f0"/><g fill="${fg}">${rects}</g></svg>`;
304-
}
305-
306-
const ICON_DIR = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="#54aeff" aria-hidden="true"><path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"/></svg>';
307-
const ICON_FILE = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="#59636e" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/></svg>';
308-
const ICON_REPO = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="#59636e" aria-hidden="true"><path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z"/></svg>';
309-
const ICON_BRANCH = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/></svg>';
310-
const ICON_TAG = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"/></svg>';
311-
// GitHub's issue-opened (green circle-dot) and issue-closed (purple check).
312-
const ICON_ISSUE_OPEN = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="#1a7f37" aria-hidden="true"><path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"/><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"/></svg>';
313-
const ICON_ISSUE_CLOSED = '<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="#8250df" aria-hidden="true"><path d="M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z"/><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z"/></svg>';
314-
// GitHub's git-pull-request (green open), git-merge (purple merged) and
315-
// git-pull-request-closed (red closed) octicons — tier 3a.
316-
const PR_PATH = 'M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z';
317-
const ICON_PR_OPEN = `<svg class="icon pr-open" width="16" height="16" viewBox="0 0 16 16" fill="#1a7f37" aria-hidden="true"><path d="${PR_PATH}"/></svg>`;
318-
const ICON_PR_TAB = `<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="${PR_PATH}"/></svg>`;
319-
const ICON_PR_MERGED = '<svg class="icon pr-merged" width="16" height="16" viewBox="0 0 16 16" fill="#8250df" aria-hidden="true"><path d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z"/></svg>';
320-
const ICON_PR_CLOSED = '<svg class="icon pr-closed" width="16" height="16" viewBox="0 0 16 16" fill="#cf222e" aria-hidden="true"><path d="M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75Zm-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-.97-.97-.97.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/></svg>';
230+
// esc + path/ref validators + formatters + SVG icons moved to
231+
// ./lib/helpers.js — imported above.
321232

322233
// ------------------------------------------------- nostr identity (2.5)
323234
// Canonical form everywhere: did:nostr:<64-hex> (did-nostr.com — exactly

0 commit comments

Comments
 (0)