Files
sanspie 69d19e501f Stage 7: documentation, help, playground; templates, aego new/bootstrap/icons, launcher
Documentation: engine/docs assembles one registry from the script
bindings, the schema, module docs and module-owned script paths; problems
are reported as warnings at build time. Module registries record doc
sources and script module owners; diag.Codes lists every diagnostic code.

Project bootstrap: project.defaultSpace ("2d"/"3d"), EnsureDirs creates
the default folders when the editor opens a project (reported in the
console), prefabs/ is a pipeline root. Native builds drop -trimpath so
binding locations resolve inside native/.

Editor files: create scene/prefab/script from menus, move/rename/duplicate
/delete with undo, script package retargeting on move, open scenes and
script panels follow renames, Save As / Rename Scene prompts, scene
picker, startup scene. Scoped shortcuts fall back to the global command
when disabled; Save As is Ctrl+Shift+S, Save All Ctrl+Alt+S.

Script editor: Go to Implementation (stub → native/ source), host
definitions always resolve to stubs, bracket match/mismatch states,
rainbow brackets (persisted), mono code font (ui-mono atlases, per-command
fonts in draw.List, PushFont/PopFont restored by Mark/Restore).

Tests: langsvc definition/implementation, bracket states, dispatch
fallback, script renames, font stack, editor file-op integration, golden
mono glyph and bracket references; depcheck rule for engine/docs.

Docs pipeline: engine, render2d and script/engine ship their own res/docs;
keyword hover text comes from the registry through manifest.Keywords;
project script symbols join the registry; aego docs export|check|build|
serve, --tool docs in native binaries, aego check --explain; app/docstool
and the tools/gendocs module (goldmark) build the static site.

Playground: app/playground + cmd/aego-playground (js/wasm, 3.3 MB gzip)
compile, highlight and run scripts in the browser; run blocks in the
language docs become examples. engine/builtin/headless is the module set
without render3d; workspace.Config.FS; StubFor/StubPaths; backends and the
script importer drop out of js builds.

VM: one frame pointer in the interpreter loop, direct host calls, panic
recovery once per run instead of per host call (Update2k -19%,
Schedule10k -24% against the previous head). bench-build/compare/profile
run pre-built, pinned, interleaved binaries.

Editor: structural bracket levels and current-block highlight, indent
guides, occurrence highlighting with write marks, multi-format clipboard
(copy/cut/paste assets, paste images as sprites or textures). gl33 probes
a renderable depth format and reports framebuffer failures by stage.

7 E:
- project templates empty/2d/2.5d/3d, app/projects as the single creation path (D148)
- launcher and recent projects (D149)
- orthographic Camera3D and Scene View (D150)
- reference states in the Inspector, ids.Short (D151)
- sceneio remaps node references when loading without KeepIDs (D152)
- exported ComponentRef<T> (D153)
- aego icons via module.Audit (D146)
2026-10-13 01:08:56 +03:00

250 lines
5.0 KiB
Go

package richtext
import (
"strconv"
"strings"
)
type Style uint8
const (
Plain Style = iota
Strong
Emphasis
Code
)
type Span struct {
Text string
Style Style
Target string
}
type Kind uint8
const (
Paragraph Kind = iota
Heading
CodeBlock
ListItem
Table
Rule
Quote
)
type Block struct {
Kind Kind
Level int
Ordinal int
Spans []Span
Lang string
Info string
Lines []string
Rows [][][]Span
Header bool
}
func (b *Block) Runnable() bool {
if b.Kind != CodeBlock {
return false
}
f := strings.Fields(b.Info)
for _, w := range f[min(1, len(f)):] {
if w == "run" {
return true
}
}
return false
}
func isFence(t string) bool {
return strings.HasPrefix(t, "```")
}
func heading(t string) (int, string, bool) {
n := 0
for n < len(t) && n < 6 && t[n] == '#' {
n++
}
if n == 0 || n >= len(t) || t[n] != ' ' {
return 0, "", false
}
return n, strings.TrimSpace(t[n+1:]), true
}
func isRule(t string) bool {
if len(t) < 3 {
return false
}
c := t[0]
if c != '-' && c != '*' && c != '_' {
return false
}
for i := 0; i < len(t); i++ {
if t[i] != c && t[i] != ' ' {
return false
}
}
return true
}
func listItem(raw string) (level, ordinal int, text string, ok bool) {
indent := 0
for indent < len(raw) && (raw[indent] == ' ' || raw[indent] == '\t') {
if raw[indent] == '\t' {
indent += 4
} else {
indent++
}
}
t := strings.TrimLeft(raw, " \t")
level = indent / 2
if len(t) >= 2 && (t[0] == '-' || t[0] == '*' || t[0] == '+') && t[1] == ' ' {
return level, 0, strings.TrimSpace(t[2:]), true
}
i := 0
for i < len(t) && t[i] >= '0' && t[i] <= '9' {
i++
}
if i > 0 && i+1 < len(t) && (t[i] == '.' || t[i] == ')') && t[i+1] == ' ' {
n, _ := strconv.Atoi(t[:i])
return level, n, strings.TrimSpace(t[i+2:]), true
}
return 0, 0, "", false
}
func indented(raw string) (string, bool) {
switch {
case strings.HasPrefix(raw, "\t"):
return raw[1:], true
case strings.HasPrefix(raw, " "):
return raw[4:], true
}
return "", false
}
func cells(t string) []string {
t = strings.TrimSpace(t)
t = strings.TrimPrefix(t, "|")
t = strings.TrimSuffix(t, "|")
var out []string
var cur strings.Builder
code := false
for i := 0; i < len(t); i++ {
c := t[i]
switch {
case c == '\\' && i+1 < len(t) && t[i+1] == '|':
cur.WriteByte('|')
i++
continue
case c == '`':
code = !code
case c == '|' && !code:
out = append(out, strings.TrimSpace(cur.String()))
cur.Reset()
continue
}
cur.WriteByte(c)
}
return append(out, strings.TrimSpace(cur.String()))
}
func separatorRow(t string) bool {
return strings.Trim(t, "|-: \t") == "" && strings.Contains(t, "-")
}
func Parse(md string) []Block {
lines := strings.Split(strings.ReplaceAll(md, "\r\n", "\n"), "\n")
var out []Block
var pend *Block
var text []string
flush := func() {
if pend != nil {
pend.Spans = Inline(strings.Join(text, " "))
out = append(out, *pend)
pend, text = nil, text[:0]
}
}
start := func(b Block, s string) {
flush()
pend = &b
text = append(text, s)
}
for i := 0; i < len(lines); i++ {
raw := lines[i]
t := strings.TrimSpace(raw)
if _, ok := indented(raw); ok && pend == nil && t != "" {
b := Block{Kind: CodeBlock}
for ; i < len(lines); i++ {
s, ok := indented(lines[i])
if !ok && strings.TrimSpace(lines[i]) != "" {
break
}
b.Lines = append(b.Lines, s)
}
i--
for len(b.Lines) > 0 && strings.TrimSpace(b.Lines[len(b.Lines)-1]) == "" {
b.Lines = b.Lines[:len(b.Lines)-1]
}
out = append(out, b)
continue
}
if lv, h, ok := heading(t); ok {
flush()
out = append(out, Block{Kind: Heading, Level: lv, Spans: Inline(h)})
continue
}
if lv, ord, s, ok := listItem(raw); ok {
start(Block{Kind: ListItem, Level: lv, Ordinal: ord}, s)
continue
}
switch {
case isFence(t):
flush()
info := strings.TrimSpace(strings.TrimPrefix(t, "```"))
b := Block{Kind: CodeBlock, Info: info}
if f := strings.Fields(info); len(f) > 0 {
b.Lang = f[0]
}
for i++; i < len(lines) && !isFence(strings.TrimSpace(lines[i])); i++ {
b.Lines = append(b.Lines, lines[i])
}
out = append(out, b)
case t == "":
flush()
case isRule(t) && pend == nil:
out = append(out, Block{Kind: Rule})
case strings.HasPrefix(t, "|"):
flush()
b := Block{Kind: Table}
for ; i < len(lines) && strings.HasPrefix(strings.TrimSpace(lines[i]), "|"); i++ {
row := strings.TrimSpace(lines[i])
if separatorRow(row) {
b.Header = len(b.Rows) == 1
continue
}
var cs [][]Span
for _, c := range cells(row) {
cs = append(cs, Inline(c))
}
b.Rows = append(b.Rows, cs)
}
i--
out = append(out, b)
case strings.HasPrefix(t, ">"):
q := strings.TrimSpace(strings.TrimPrefix(t, ">"))
if pend == nil || pend.Kind != Quote {
start(Block{Kind: Quote}, q)
} else {
text = append(text, q)
}
case pend == nil:
start(Block{Kind: Paragraph}, t)
default:
text = append(text, t)
}
}
flush()
return out
}