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)
250 lines
5.0 KiB
Go
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
|
|
}
|