# Glyph Foundry

A dependency-free Unicode 18 bitmap-glyph editor and Synchronet-hosted submission service for SyncTERM.

The editor treats a glyph identifier as one Unicode 18 extended grapheme cluster in NFC, rather than as a single code point or JavaScript character. That supports canonically composed and decomposed text, combining marks, Indic conjuncts, emoji presentation sequences, skin-tone modifier sequences, flags, tag sequences, and RGI ZWJ sequences without losing their underlying encoding.

The set of possible grapheme clusters is open-ended, so the sidebar pre-enumerates the finite scalar and standardized emoji repertoires while **Add any grapheme cluster** accepts pasted text or `U+` notation. Input is normalized with Unicode 18 normalization data, checked with Unicode 18 grapheme-break properties, and stored under the resulting normalized sequence. User-added clusters persist locally and return to the work queue after reload.

Each submission records:

- the Unicode version, NFC cluster text, exact code-point sequence, and normalized UTF-8 bytes;
- an explicit 8- or 16-pixel terminal cell width;
- monochrome 8/14/16-height bitmaps;
- color 8/14/16-height bitmaps for emoji.

The editors model the classic 640×400 raster displayed on a 4:3 screen. This gives individual pixels a physical width-to-height ratio of 5:6 rather than 1:1. Every monochrome and color editor includes a live, grid-free preview using that same display geometry.

Grid guides mark CP437 typographic metrics derived from the occupied rows of `A`, `H`, `x`, `g`, `j`, `p`, `q`, and `y`. X-height and baseline are emphasized; cap-height and descender boundaries are dashed. In the 8-row font, cap-height and descender already coincide with the grid extents, so redundant interior lines are omitted.

For emoji, each height can be copied between its monochrome and color forms. Text-to-color conversion uses the currently selected palette color; color-to-text conversion treats every non-transparent pixel as set. These conversions participate in undo and redo.

## SyncTERM font data

Existing glyphs begin with the actual default SyncTERM font data rather than a blank canvas. The imported size-specific stack matches SyncTERM's defaults:

- 8×8: SyncTERM UNSCII 8, with SyncTERM Emoji filling missing monochrome glyphs;
- 8×14: SyncTERM CP437 English, with SyncTERM Emoji filling missing monochrome glyphs;
- 8×16 and 16×16: SyncTERM Unicode (GNU Unifont with the UNSCII overlay), with SyncTERM Emoji filling missing monochrome glyphs.

Native monochrome artwork takes priority, matching SyncTERM's presentation-font behavior. Noto supplies the RGBA color form even when the monochrome form comes from a native base font. A saved browser draft remains authoritative; **Reload SyncTERM glyph** replaces all six current slots with the imported source data and participates in undo/redo. The reference description records the source SyncTERM commit.

To refresh `syncterm-font-data.js`, pass the SyncTERM Git root to the importer:

```sh
python3 tools/import-syncterm-fonts.py /bbsdev/syncterm
```

The importer reads SyncTERM's checked-in generated C sources, so it does not download fonts or require Pillow, fontTools, or HarfBuzz. It also refreshes the redistributed font licenses and upstream notices in `data/syncterm-fonts`, and updates the font bundle's commit-based cache key in `index.html`.

## Run locally

```sh
python3 -m http.server 8080
```

Then open <http://localhost:8080>.

You can also open `index.html` directly with a `file://` URL. The generated `unicode-data.js` bundle exists specifically for browsers that block local `fetch()` calls.

The complete editor and browser-local drafts work under `file://`. Creating a SourceForge submission requires the page to be installed on a Synchronet web server; the bearer token is never available to a locally opened page or to any browser JavaScript.

## Unicode data

The repository vendors the official Unicode 18.0.0 name, width, normalization, grapheme-break, Indic-conjunct, emoji-property, and emoji-test data under `data/unicode-18.0.0`. The interface expands the derived names into 172,808 searchable scalar-value records and adds all 3,972 component or fully-qualified Emoji 18 entries. Unicode 18 data is also used directly for NFC normalization and extended grapheme segmentation, so behavior does not depend on the Unicode version implemented by the browser. Wide/fullwidth characters and emoji default to 16-pixel cells; other clusters default from their first spacing scalar. Contributors can override either width.

The Unicode data is covered by [the included Unicode license](data/unicode-18.0.0/LICENSE.txt).

After replacing any source data file, rebuild the browser bundle with:

```sh
node tools/build-unicode-bundle.mjs
```

The Unicode core regression checks can be run with:

```sh
deno run --allow-read tests/unicode-core.test.mjs
deno run --allow-read tests/syncterm-fonts.test.mjs
```

## Contributor identity and submissions

Glyph Foundry does not maintain a password database. A contributor chooses a display name and may add a SourceForge username; that profile remains in browser `localStorage` and becomes public attribution on submitted tickets. Drafts, custom clusters, and the browser's submission receipts are also local. The Synchronet server durably records each accepted submission in SQLite before contacting SourceForge.

The browser sends a versioned JSON record to `api/submit.ssjs`. It contains the normalized cluster identity, explicit cell width, all three monochrome bitmaps, and all three color bitmaps when color is enabled for an emoji. The server validates dimensions and values, rate-limits new submissions, and uses a client-generated UUID for idempotency. `api/status.ssjs` lets the originating browser update its local receipt.

Each SourceForge ticket contains human-readable metadata, monochrome text previews, and an attached PNG sheet showing every monochrome and color size at the same 5:6 displayed pixel geometry as the editor. The description embeds that attachment for immediate review and ends with the authoritative JSON record. Temporary SourceForge failures leave the record in an SQLite outbox. The retry worker searches for the UUID marker before resending, preventing an ambiguous timeout from creating duplicate tickets.

## Synchronet deployment

Install the repository contents together under the selected Synchronet web root. The web server account must be able to create and update `system.data_dir/glyph-foundry/submissions.sqlite3`.

For an existing remote web directory, the deployment helper refreshes the bundled fonts from the neighboring `../syncterm` Git checkout and then copies the runtime files with `scp`:

```sh
tools/deploy.sh admin@example.com:/sbbs/web/root/glyph-foundry
```

The remote destination must already exist. The helper does not copy or modify the server's configuration, token, SQLite data, or timed-event configuration.

Create `ctrl/modopts/glyph-foundry.ini` with no section header:

```ini
SourceForgeProject=syncterm
SourceForgeTracker=glyphs
SourceForgeToken=YOUR_BEARER_TOKEN
RequestTimeout=15
```

Alternatively, place the same keys under a `[glyph-foundry]` section in the shared `ctrl/modopts.ini`. Synchronet's `modopts.js` checks the module-specific file first, so it takes precedence when both exist.

Generate the bearer token at <https://sourceforge.net/auth/oauth/> while signed in as the account that will own the tickets. A dedicated service account is strongly recommended: grant it permission to create tickets in the Glyphs tracker, without project administration or unrelated repository access. The token inherits all permissions of its account, so keep the file out of the web root and revoke the token immediately if it is exposed.

The submission target is derived from the configured slugs and defaults to:

```text
https://sourceforge.net/rest/p/syncterm/glyphs/new
```

Configure a Synchronet timed event to run `server/retry-submissions.js` with `jsexec` every five minutes. Immediate delivery does not depend on the event; it drains submissions that remained queued after a timeout, rate limit, or SourceForge outage. Authentication and permission failures are marked blocked instead of retried indefinitely.

## Tests

Run the browser-independent regression checks with:

```sh
deno run --allow-read tests/unicode-core.test.mjs
deno run --allow-read tests/syncterm-fonts.test.mjs
deno run --allow-read tests/submission-core.test.mjs
```

The submission suite covers narrow and wide glyphs, all three monochrome and color heights, ZWJ/modifier identity, validation failures, SourceForge form encoding, and ticket-number extraction. An end-to-end production smoke test requires a configured token and intentionally creates a public ticket, so it is not run automatically.
