Skip to content

[pull] master from GaijinEntertainment:master#991

Merged
pull[bot] merged 24 commits into
forksnd:masterfrom
GaijinEntertainment:master
May 14, 2026
Merged

[pull] master from GaijinEntertainment:master#991
pull[bot] merged 24 commits into
forksnd:masterfrom
GaijinEntertainment:master

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented May 14, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

borisbat and others added 24 commits May 13, 2026 12:28
Lands the new "Forge" design across the daslang.io site and replaces three
mismatched highlighters with one CodeMirror mode driven by the canonical
lexer keyword set.

Site:
- Landing rewrite (forge.css/forge.js/index.html): Forge tokens, hero
  terminal panel with three live samples, dasProfile bench renderer,
  install tabs, news feed, ecosystem footer.
- /playground/ skinned in Forge (forge-skin.css over web/ui's main.css).
- Blog: 25 posts migrated from borisbat/dascf-blog with Hexo-style
  front matter, build_blog.py renders HTML + RSS + changelist.
- /news/ from _news/*.md (27 entries, oldest 2019 -> newest 2026-05-09).
- /downloads.html for binary releases.

Highlighters unified:
- Vendored CodeMirror 5.65.16 + simple-mode addon under site/files/cm/.
- Single keyword module (daslang-keywords.js) sourced from ds2_lexer.lpp
  (105 reserved words); used by both daslang-mode.js (CodeMirror) and
  highlight.js (blog tokenizer).
- cm-forge.css theme maps CM token classes to Forge tokens; replaces
  the eclipse theme + JS-heuristic mode previously used by /playground/.
- Hero editor is now a CodeMirror instance (drops ~150 lines of
  contenteditable wiring); "playground" button trampolines current
  source into /playground/ via #code= URL hash.
- Pygments daslang lexer in doc/source/daslang.py refreshed:
  removed move/move_new/nothing (no longer real), added
  capture/static_elif/template/const/default/typedecl/uninitialized.

Sphinx docs reskinned via doc/source/_static/custom.css + forge-logo.

CI (.github/workflows/pages.yml): builds WASM via Emscripten, copies
site/files (including cm/ bundle), renders blog + news, snapshots
dasProfile JSON, drops everything into the Pages artifact.
Phase 1 of the playground multi-file plan: lock the test harness shape
before the real features land. No production code touched.

- site/tests/playground/ — Playwright config + fixtures + one smoke spec
  asserting the page loads with the Forge-themed CodeMirror instance and
  the daslang mode is tokenizing source.
- .github/workflows/playground-e2e.yml — PR + branch-push CI gate. Builds
  the site without WASM (skipping the 5-10 min Emscripten step), boots
  python -m http.server against _site/, runs the suite minus any spec
  tagged @wasm. Uploads playwright-report/ on failure.
- site/.gitignore — tests/playground/{node_modules,playwright-report,
  test-results,blob-report}/
- .gitignore — .playwright-mcp/ scratch dir from the local MCP

Subsequent phases will land their specs alongside their features
(dropdowns, tabs, share button, hero handoff).
Phase 2: the Examples + Tests <select>s were populated correctly but
never had a change handler, so picking an option did nothing. selectSample
itself had a latent bug — `vv !== NaN` is always true, so any non-numeric
value would crash on the lookup.

- Add `onchange="selectSample('examples'|'tests')"` to both <select>s.
- Fix the NaN check (`!Number.isNaN`) and validate the index range up
  front; bail cleanly on the "Select..." sentinel.
- Refactor selectSample to load every entry of `files[]` in parallel
  (samples have always shipped a multi-file schema even though loaders
  only ever read files[0]). The bundle is handed to a new `loadSample`
  helper that surfaces main.das (or the first file) in the single CM
  instance and stashes the full bundle on window for phase 3's tab strip
  to pick up.
- web/ui/src/main.js is the canonical source — site/playground/main.js
  is a gitignored CI re-sync, so the patch goes upstream.
- site/tests/playground/dropdowns.spec.js asserts both dropdowns swap
  the editor buffer and the sentinel-reset behavior holds.
Phase 3: the playground now edits multiple files. CodeMirror.Doc per file,
editor.swapDoc on tab click, full add/delete/rename UI, localStorage
autosave, URL-hash override.

Tab strip (replaces the hardcoded main.das label in the toolbar):
- Click tab → switch active buffer (in-place class toggle to avoid
  destroying the DOM mid-dblclick).
- Click + → adds untitled{N}.das, switches to it.
- Double-click name → inline rename input; Enter commits, Escape cancels.
- Click × → confirm if non-empty, delete, fall back to main.das.
- main.das is fixed: × disabled, rename rejected, delete refused.

Multi-file run (main.js): runCode iterates every pgState file via
FS.writeFile before Module.callMain('main.das'). Same MEMFS, same
require resolution, just N files instead of one.

URL hash + autosave priority:
- `#code=<percent>` (hero handoff) → wrapped into main.das.
- `#z=<lz-base64>` (share button — phase 6) → full multi-file payload.
- localStorage `daslang.playground.state.v1` autosaves debounced (250ms),
  restored only when no hash is present.
- pgRestoredFromState flag tells main.js to skip its default
  selectSample("examples", 0), which otherwise async-clobbers the
  restored state ~200ms after page load.

playground-init.js extended: dispatches on hash prefix, stashes payload
to __pendingSampleBundle for pgInit to pick up.

forge-skin.css: .pg-tab / .pg-tab--active / .pg-tab__close /
.pg-tab__rename / .pg-tab__add styling using Forge tokens.

Specs (all green, 15/15 in the full suite):
- tabs.spec.js: 8 cases covering add / switch / rename / delete /
  main-protection / dialog flows.
- persistence.spec.js: 2 cases for 3-file reload survival + hash
  override.
Phase 4: vendors tutorials/macros/09_for_loop_macro.das + its companion
for_loop_macro_mod.das into web/ui/samples/examples/macros/ and registers
the pair as "Macros (multi-file)" in samples/data.json. Picking it from
the Examples dropdown populates both tabs.

This is the wedge case for multi-file: macros must live in a separate
module from the one that uses them, so a single-file playground simply
cannot demonstrate them. Running the sample emits the `for ((k,v) in tab)`
expansion — a real macro module compiled live in the browser.

macro-sample.spec.js covers three cases:
- two tabs appear after picking the sample (no-WASM)
- main.das contains `require for_loop_macro_mod` + macro syntax (no-WASM)
- ▶ run produces section-3 output `apple => 10` (@wasm-tagged; skipped
  by the no-WASM CI gate)
Phase 5: the runCode loop landed back in phase 3 (write every pgState
file to MEMFS before callMain). This adds the spec that exercises it:

- runs a user-authored two-file program: writes main.das requiring utils
  with a `hello()` print, asserts the output panel shows "hi, playground".
- renaming utils.das breaks the build: after pgRenameFile, `require utils`
  should fail at compile time, surfaced through stderr.

Both tagged @wasm so the no-WASM CI gate skips them.
Phase 6: a "share this playground" button next to ▶ run. Clicking opens
a small popover with a shareable URL of the current state, [Copy], and
optional [Shorten to is.gd].

URL encoding: every file in pgState + the active filename serialize as
JSON, then LZString.compressToEncodedURIComponent → `#z=<base64ish>`.
URL-safe out of the box; a 3-file ~40-line state lands well under 1 KB.

is.gd shortener: GET https://is.gd/create.php?format=simple&url=… returns
the short URL as plain text with permissive CORS. On success the input
swaps to the short form; on failure the button briefly says "failed"
and the long URL stays put.

Vendored: site/files/lz-string.min.js (~5 KB, MIT, lz-string@1.5.0).

forge-skin.css: .button_header--ghost variant for the share button;
.pg-share popover (header, URL row, copy/shorten/footer).

playground-init.js already dispatches `#z=`/`#code=` to pgLoadFiles from
phase 3 — this commit just produces those URLs.

share.spec.js: four cases.
  - share popover surfaces the URL with all files (decode round-trip)
  - opening the URL in a fresh context restores state (one retry, since
    new-context bootstrap can race the polling pgInit under high
    parallelism)
  - is.gd shorten replaces the URL on success (route() stubs the request)
  - shorten failure surfaces the error and preserves the long URL
Phase 7: the landing hero's "↗ playground" button has been there since
the prior PR and emits `playground/index.html#code=<percent-encoded>` —
the legacy single-file format. After the multi-file rework that hash
shape still has to land cleanly in main.das.

hero-handoff.spec.js opens the landing page, plants a marker into the
hero's CodeMirror via the CM JS API, clicks the handoff button, captures
the popup page, and asserts /playground/ comes up with a single main.das
tab carrying the marker.

Full suite: 25 specs locally (3 @wasm-tagged), 22 in the CI no-WASM tier.
- Single top toolbar: [tabs] [+] [examples] [share] [run] on the left,
  [clear] on the right. Drop the per-panel "output" label and the Tests
  dropdown. Random Sequence moves into Examples; other test samples
  are removed.
- New .main_workspace flex-row contains [code | output] split by a
  draggable handle (11px hit area, 1px line, amber on drag). Position
  persists to localStorage; double-click resets to 50/50.
- The first splitter pass looked correct but did nothing in real
  browsers: .main_col carried `flex: 1 1 0 !important`, which !important
  beats the splitter's inline `style.flex = '0 0 N%'`. Switch to a
  higher-specificity .main_workspace > .main_col selector with no
  !important so the inline override actually applies.
- Splitter spec drives page.mouse.down/move/up and asserts the
  *measured* column width — the synthetic-event flavor passed even
  while the layout never moved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "source: github.com/borisbat/dasProfile" line under the
benchmarks panel was plain text. Wrap it in an <a> with a new
`.forge-bench__source` style — amber-dim by default (subtle enough
to sit next to fg-faint meta text), full amber + underline on hover.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hook up the legacy dascf-blog Disqus account (shortname
"https-borisbat-github-io-dascf-blog") under every blog post. Identifier
pins to the post slug so threads stay stable across dev/prod URLs and
future renames; old comments stay tied to the original blog URLs in
the Disqus admin (can be migrated later via Disqus' URL Mapper).

- build_blog.py: render_comments() emits a .forge-post__comments
  section after the article on post pages only (not index/news/
  changelist).
- forge.css: section wrapper styled to match Forge (top divider,
  amber "§ comments" label, dark bg-2 thread container with rule
  border). `color-scheme: dark` on :root hints Disqus to render its
  Auto theme as dark.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two surfaces depend on a real keyboard and (for playground) a 5 MB WASM
bundle that's mediocre on a phone. On <768px viewports we now:

- Landing hero: hide the live CodeMirror panel + sample tabs + run /
  playground buttons. A static <pre><code class="language-daslang">
  block, tokenized by the existing highlight.js, takes its place. The
  terminal frame (dots + filename) is kept so the visual identity stays
  intact. forge.js short-circuits the CM init on mobile so the editor
  doesn't materialize behind the static block.
- /playground/: a synchronous mobile detector tags <html> with
  `.is-pg-mobile` so CSS hides the IDE chrome, and on DOMContentLoaded
  overrides `pageInit` so the body.onload bootstrap renders a Forge-
  styled "open on a laptop" notice instead of fetching daslang_static.js
  / .wasm. Spec asserts zero requests for either resource on mobile.

The (already-hidden-on-mobile) playground nav link in .forge-nav__links
stays as-is; the redundant "playground" version label on the playground
page is also hidden when the notice is showing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Punch list from a full-site audit:

Block fixes
- Mobile nav: ≡ hamburger button in the right-side chip strip on every
  Forge nav (landing, downloads, blog template, playground). Toggles
  `.forge-nav.is-open`, which reveals `.forge-nav__links` as an
  absolutely-positioned dropdown panel under the nav. Phone users can
  now reach docs / benchmarks / downloads / blog / community.
- Mobile blog/changelist grid: `.forge-blog-item` carried only its
  desktop `140px 90px 1fr` shape, which squeezed titles into ~94px at
  390px viewport (one word per line). Added a mobile override stacking
  date+tag on row 1, body on row 2.
- Missing image in blog/instruments: `/images/call_tree.PNG` is a
  Hexo-era absolute path with no asset on this site. Rewrote the
  sentence to drop the inline image.

Cleanup
- Removed dead `runTests`/`runTest`/`outputPool` machinery from
  web/ui/src/main.js (and the mirrored site/playground/main.js) — the
  Tests dropdown that drove them is gone. Simplified the dropdown
  populate loop now that only Examples remains.
- Deleted orphan CodeMirror files from web/ui/src/: codemirror.css,
  codemirror.min.js, eclipse.css. The playground now loads the
  Forge-themed CodeMirror bundle from /files/cm/ — these were ~232 KB
  of dead bytes copied per CI deploy. site/.gitignore entries for the
  same files dropped.
- Hide `.forge-nav__version` at <480 px so the version label stops
  crowding [github ↗] on narrow phones.
- Fixed "0.6.0-RC1 0.6.0-RC2" double label in the 2026-02-28 news
  entry (left over from a partial RC1→RC2 edit).
- Sphinx `version` was '0.6' while the site shows v0.6.2; bumped to
  '0.6.2' to match.
- hero-handoff.spec.js: added `retries: 1` for the two-window race
  that flakes under high parallelism (passes alone, fails ~1/3 in
  full suite). Suite is green-on-retry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uite

The audit flagged the README as stale — all three recently-landed surfaces
(mobile fallbacks, multi-file playground, Disqus comments) had no mention.
Updated:

- Replaced the desktop-only summary with a "Surface map" table covering
  hero / playground / blog / nav behavior on desktop vs <768 px.
- Expanded the playground section: multi-file tab CRUD, ▶ run via MEMFS
  write loop, ↗ share (#z= compressed hash + is.gd shorten), 250 ms
  autosave to localStorage, splitter persistence.
- Added a Playwright suite section: 28 specs, no-WASM in ~5 s,
  pointer to the CI workflow.
- Refreshed the file tree under site/ — new playground-*.js files,
  the site/tests/playground/ suite, the relocated CodeMirror bundle at
  site/files/cm/, and lz-string.min.js.
- Updated the "/playground/ blank page" gotcha now that CodeMirror lives
  in /files/cm/ rather than web/ui/src/.
- Added a mobile-gate gotcha (hard-refresh required after toggling
  DevTools' device toolbar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 1 from Copilot. All four findings were real.

- pages.yml + playground-e2e.yml: stage every `playground-*.js` (init,
  tabs, share, splitter) not just init.js. CI's 404 storm on
  playground-tabs/share/splitter is the root cause of the failed
  playground-e2e run; same bug in pages.yml would have landed the
  regression in prod. Globbing also picks up future additions.
- playground-tabs.js: drop `/` from the rename regex. The previous
  pattern accepted `dir/foo.das`, but Emscripten MEMFS's `FS.writeFile`
  doesn't auto-create parents — Run would silently ENOENT. Flat
  namespace is enough for the playground.
- forge.js escapeHtml: also escape `"` and `'`. The news renderer
  interpolates `n.link` inside `href="..."`; a `"` in the URL would
  break out and inject markup. `_news/*.md` is committed so practical
  risk is near-zero, but the fix is one line and defensive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two more from Copilot. Both real.

- Atom feed default `--site-url` was `https://dascript.org`; neither
  workflow passed `--site-url`, so `blog/feed.xml` would have advertised
  old-domain URLs once deployed at daslang.io. Subscribers' clients
  follow `<link>`/`<id>` URLs to fetch posts — those would have hit a
  domain that no longer serves the site. Bumped the default to
  daslang.io AND pinned `--site-url https://daslang.io` from both
  workflows so the default can't silently drift again. Updated the
  stale README header that still pointed at dascript.org.
- runCode() wrote every current pgState file to MEMFS but never
  unlinked files the user had deleted/renamed. Concrete bug: open
  utils.das, write `require utils` in main, run (writes utils.das to
  MEMFS), then delete utils.das tab, run again — still works from
  stale MEMFS state, executed program no longer matches visible tabs.
  Fixed by tracking a module-level Set of last-written names; each
  run unlinks stale-from-prior-run names (ENOENT tolerated) before
  writing current. Mirrored to site/playground/main.js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three real, one false alarm.

Real:
- doc/source/_static/custom.css: `@import` for Inter Tight + JetBrains
  Mono lived AFTER a `:root` rule, so browsers silently drop it (CSS 2.1
  §6.3). Docs were quietly falling back to system fonts. Moved the
  import to the very top.
- playground-init.js + hero-handoff.spec.js: the hash-payload dispatch
  stashed `__pendingSampleBundle` but never set `pgRestoredFromState`.
  main.js's default `selectSample("examples", 0)` then ran and the
  data.json fetch sometimes beat pgLoadFiles, overwriting the marker
  text with hello.das. Visible as a flake in CI (slower http.server)
  but a real correctness bug on cold-cache loads in prod too. Set the
  flag; also harden the spec to wait for the marker content, not just
  for pgState's structure to materialize. Three consecutive local runs
  green.
- web/ui/index.html: legacy standalone IDE entry point referencing
  files we deleted (codemirror.{min.js,css}, eclipse.css). Nothing in
  CI/build references it. Removed.

False alarm:
- site/files/cm/daslang-keywords.js: Copilot flagged enum / typedef /
  with / aka / reinterpret / upcast / range64 / urange / urange64 as
  absent. They're all present (verified against
  src/parser/ds2_lexer.lpp's `DAS_*` returns). Reply on the thread —
  no code change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…stage

playground-tabs.js: tryInit() polled every 30ms for `window.code` indefinitely.
On mobile, pageInit is short-circuited so the variable never appears, leaving
a 33Hz setTimeout chain running for the life of every mobile tab. Skip the
init when the .is-pg-mobile gate class is present.

pages.yml: the WASM build step keeps continue-on-error so docs/blog still
ship through emsdk hiccups, but the staging block then accepted a half-built
web/output and deployed a playground whose Run button 404'd the runtime.
Tighten the gate to require both daslang_static.{js,wasm} on disk before
staging any playground files — Pages preserves the prior deploy, so the
visible /playground/ keeps working instead of going silently broken.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-miss placeholder

playground-init: the `#z=` hash decode stashed `__pendingSampleBundle =
payload.files` but dropped `payload.active`. When playground-tabs.js's
tryInit consumed the bundle before this script's tryApply loop resolved,
`pgLoadFiles(bundle)` ran without an active argument and fell back to
main.das, so a shared URL whose author was viewing utils.das landed on the
recipient's screen with main.das in front. Stash `__pendingSampleActive`
alongside the bundle and consume it in both tab-init paths.

pages.yml: round-4 staging gate (require daslang_static.{js,wasm} before
copying playground files) was based on an incorrect read of
actions/deploy-pages — Pages publishes _site as a complete snapshot, not a
layer over the prior deploy, so a missing _site/playground/ 404s the route
instead of preserving the previous runtime. Stage site/playground/
placeholder.html into _site/playground/index.html on the missing-artifact
branch so /playground/ keeps resolving to something useful while the next
merge re-rolls the runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
License footer in site/index.html, site/downloads.html, and
site/blog/template.html displayed "MIT" but the repository LICENSE is
BSD 3-Clause (Gaijin Entertainment, 2019-2023). The blog template fixes
every generated blog page on the next build, so no per-page edits needed.

playground-tabs.js: pgSwitchFile updated pgState.active and swapped the
visible Doc but never called autosave(). User-initiated tab clicks
therefore didn't persist; a page reload restored the previous active tab
(or main.das if no content edit had triggered autosave on `change` yet).
The other state-changing entry points (pgAddFile/Delete/Rename/LoadFiles)
were already saving correctly — only the bare tab-click path was leaking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…redesign

site: Forge redesign — landing, blog, playground, mobile
Documentation: introduce an "External modules" section in the docs TOC
(after stdlib) with a single dasImgui entry. Cross-link points at
borisbat.github.io/dasImgui/ where the matching theme + intersphinx-based
backlink land in a sibling PR.

News: 0.6.2 release announcement for the landing-page § 05 strip — the
release was promoted on 2026-05-13.

mouse-data: four cards from this session's wrap-up curation. GitHub Pages
deploy-snapshot semantics, CSS @import ordering rule, !important-vs-inline
debugging anchor, CodeMirror 5 multi-Doc autosave discipline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
web/step0_emsdk_install.sh is a one-liner that just clones the emsdk
wrapper repo. The toolchain itself — emsdk/upstream/emscripten/, which
the cmake -DCMAKE_TOOLCHAIN_FILE arg points at — lands via the
install+activate pair that step1_emsdk_activate_linux.sh does locally.
pages.yml was calling step0 but never the install/activate steps, so the
WASM build always failed at cmake-configure ("Could not find toolchain
file"). continue-on-error: true on this step paints it green in the run
summary, and the post-build staging silently skipped the missing-artifact
copies — until the round-5 placeholder gate started serving the
"Runtime rebuild in progress" page instead, which is what surfaced this.

Add the two missing commands. The build should now actually produce
web/output/daslang_static.{js,wasm}; the existing staging gate will then
take the real path instead of the placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…al-modules-dasimgui

docs: External modules section + dasImgui crosslink
@pull pull Bot locked and limited conversation to collaborators May 14, 2026
@pull pull Bot added the ⤵️ pull label May 14, 2026
@pull pull Bot merged commit edc9017 into forksnd:master May 14, 2026
@pull pull Bot had a problem deploying to github-pages May 14, 2026 02:58 Error
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant