diff --git a/.forgejo/workflows/docs.yml b/.forgejo/workflows/docs.yml new file mode 100644 index 00000000..60b78746 --- /dev/null +++ b/.forgejo/workflows/docs.yml @@ -0,0 +1,55 @@ +name: docs + +# Rebuild the documentation site and publish it to the `pages` branch on every +# push to main that touches the docs sources. The site is generated from the +# per-symbol source of truth in docs/language/** — nothing is hand-edited on the +# pages branch. +on: + push: + branches: [main] + paths: + - 'docs/**' + - 'tools/docgen/**' + - '.forgejo/workflows/docs.yml' + workflow_dispatch: {} + +permissions: + contents: write + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate the documentation site + run: | + python3 --version + python3 tools/docgen/gen.py --out public + echo "--- generated files ---" + ls -la public + + - name: Sanity-check the output (root has index.html + .nojekyll, no broken links) + run: python3 tools/docgen/check.py public + + - name: Publish to the pages branch + run: | + set -eu + TOKEN="${PAGES_TOKEN:-${AUTO_TOKEN:-}}" + if [ -z "$TOKEN" ]; then + echo "::error::No deploy token. Add a repo secret PAGES_TOKEN (write access) or allow the automatic token to push." + exit 1 + fi + cd public + git init -q -b pages + git config user.name "ludic-docs-bot" + git config user.email "docs@workshopsoft.io" + git add -A + git commit -q -m "docs: regenerate site from ${SOURCE_SHA}" + git push -f "https://ludic-docs-bot:${TOKEN}@git.workshopsoft.io/workshopsoft/ludic.git" pages + echo "published $(git rev-parse --short HEAD) to pages" + env: + PAGES_TOKEN: ${{ secrets.PAGES_TOKEN }} + AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SOURCE_SHA: ${{ github.sha }} diff --git a/docs/language/annotations/_section.md b/docs/language/annotations/_section.md new file mode 100644 index 00000000..5da3cb23 --- /dev/null +++ b/docs/language/annotations/_section.md @@ -0,0 +1,7 @@ +--- +id: annotations +title: Annotations +order: 13 +--- + +@Name(...) decorators attach compile-time behavior to a handler, property, or function. diff --git a/docs/language/annotations/annot-computed.md b/docs/language/annotations/annot-computed.md new file mode 100644 index 00000000..5bcb92cf --- /dev/null +++ b/docs/language/annotations/annot-computed.md @@ -0,0 +1,12 @@ +--- +id: annot-computed +name: @Computed +category: annotations +kind: annotation +tokens: @Computed +sig: @Computed on a property field +tip: A derived field: an expression expanded inline wherever it is read, never stored. +order: 1 +--- + +A derived field: an expression expanded inline wherever it is read, never stored. diff --git a/docs/language/annotations/annot-export.md b/docs/language/annotations/annot-export.md new file mode 100644 index 00000000..79a496ce --- /dev/null +++ b/docs/language/annotations/annot-export.md @@ -0,0 +1,12 @@ +--- +id: annot-export +name: @export +category: annotations +kind: annotation +tokens: @export +sig: @export fn name(…) -> R +tip: Expose a function as a C-ABI symbol from a module. +order: 3 +--- + +Expose a function as a C-ABI symbol from a module. diff --git a/docs/language/annotations/annot-on.md b/docs/language/annotations/annot-on.md new file mode 100644 index 00000000..acb4f5a6 --- /dev/null +++ b/docs/language/annotations/annot-on.md @@ -0,0 +1,12 @@ +--- +id: annot-on +name: @OnEnable / @OnDisable / @OnDespawn +category: annotations +kind: annotation +tokens: @OnEnable @OnDisable @OnDespawn +sig: @OnDisable(Prop) handler … { … } +tip: Lifecycle hooks that fire when a property/model is toggled or an entity is torn down. +order: 2 +--- + +Lifecycle hooks that fire when a property/model is toggled or an entity is torn down. diff --git a/docs/language/annotations/annot-queries.md b/docs/language/annotations/annot-queries.md new file mode 100644 index 00000000..1ef62402 --- /dev/null +++ b/docs/language/annotations/annot-queries.md @@ -0,0 +1,12 @@ +--- +id: annot-queries +name: @Queries +category: annotations +kind: annotation +tokens: @Queries +sig: @Queries(these: [Pos, Vel]) +tip: Declare the properties a handler touches, binding their fields by name in the body. +order: 0 +--- + +Declare the properties a handler touches, binding their fields by name in the body. diff --git a/docs/language/annotations/annot-sync.md b/docs/language/annotations/annot-sync.md new file mode 100644 index 00000000..a01629bb --- /dev/null +++ b/docs/language/annotations/annot-sync.md @@ -0,0 +1,12 @@ +--- +id: annot-sync +name: @Sync / @Owned +category: annotations +kind: annotation +tokens: @Sync @Owned +sig: @Sync property … / @Owned +tip: Mark a property's fields as replicated, and entities as owned, for networking. +order: 4 +--- + +Mark a property's fields as replicated, and entities as owned, for networking. diff --git a/docs/language/builtins/_section.md b/docs/language/builtins/_section.md new file mode 100644 index 00000000..d26212f5 --- /dev/null +++ b/docs/language/builtins/_section.md @@ -0,0 +1,7 @@ +--- +id: builtins +title: Builtin functions +order: 12 +--- + +Global functions available anywhere, beyond the namespaced APIs above. diff --git a/docs/language/builtins/fn-abs.md b/docs/language/builtins/fn-abs.md new file mode 100644 index 00000000..e5907fd9 --- /dev/null +++ b/docs/language/builtins/fn-abs.md @@ -0,0 +1,12 @@ +--- +id: fn-abs +name: abs +category: builtins +kind: builtin +tokens: abs +sig: abs(a) -> int +tip: Absolute value. +order: 8 +--- + +Absolute value. diff --git a/docs/language/builtins/fn-bytes.md b/docs/language/builtins/fn-bytes.md new file mode 100644 index 00000000..97f2fb92 --- /dev/null +++ b/docs/language/builtins/fn-bytes.md @@ -0,0 +1,12 @@ +--- +id: fn-bytes +name: bytes / words +category: builtins +kind: builtin +tokens: bytes words +sig: bytes(n) -> ptr / words(n) -> words +tip: Allocate a raw buffer of n bytes / n 32-bit words. +order: 11 +--- + +Allocate a raw buffer of n bytes / n 32-bit words. diff --git a/docs/language/builtins/fn-clamp.md b/docs/language/builtins/fn-clamp.md new file mode 100644 index 00000000..69a0ec43 --- /dev/null +++ b/docs/language/builtins/fn-clamp.md @@ -0,0 +1,12 @@ +--- +id: fn-clamp +name: clamp +category: builtins +kind: builtin +tokens: clamp +sig: clamp(v, lo, hi) -> int +tip: Constrain v to [lo, hi]. +order: 9 +--- + +Constrain v to [lo, hi]. diff --git a/docs/language/builtins/fn-font_load.md b/docs/language/builtins/fn-font_load.md new file mode 100644 index 00000000..21b8833f --- /dev/null +++ b/docs/language/builtins/fn-font_load.md @@ -0,0 +1,12 @@ +--- +id: fn-font_load +name: font_load +category: builtins +kind: builtin +tokens: font_load +sig: font_load(path) -> int +tip: Load a TrueType font and return a handle for the UI. +order: 12 +--- + +Load a TrueType font and return a handle for the UI. diff --git a/docs/language/builtins/fn-fx.md b/docs/language/builtins/fn-fx.md new file mode 100644 index 00000000..cd6067bd --- /dev/null +++ b/docs/language/builtins/fn-fx.md @@ -0,0 +1,12 @@ +--- +id: fn-fx +name: fx / flr +category: builtins +kind: builtin +tokens: fx flr +sig: fx(n) -> fixed / flr(x) -> int +tip: Convert between int and fixed-point. +order: 10 +--- + +Convert between int and fixed-point. diff --git a/docs/language/builtins/fn-len.md b/docs/language/builtins/fn-len.md new file mode 100644 index 00000000..9b4c5080 --- /dev/null +++ b/docs/language/builtins/fn-len.md @@ -0,0 +1,12 @@ +--- +id: fn-len +name: len +category: builtins +kind: builtin +tokens: len +sig: len(x) -> int +tip: Length of a slice or string. +order: 5 +--- + +Length of a slice or string. diff --git a/docs/language/builtins/fn-load.md b/docs/language/builtins/fn-load.md new file mode 100644 index 00000000..ce938ad3 --- /dev/null +++ b/docs/language/builtins/fn-load.md @@ -0,0 +1,12 @@ +--- +id: fn-load +name: load +category: builtins +kind: builtin +tokens: load +sig: load() -> bool +tip: Restore a snapshot written by save(). +order: 4 +--- + +Restore a snapshot written by save(). diff --git a/docs/language/builtins/fn-min.md b/docs/language/builtins/fn-min.md new file mode 100644 index 00000000..b215df4b --- /dev/null +++ b/docs/language/builtins/fn-min.md @@ -0,0 +1,12 @@ +--- +id: fn-min +name: min / max +category: builtins +kind: builtin +tokens: min max +sig: min(a, b) / max(a, b) -> int +tip: The smaller / larger of two ints. +order: 7 +--- + +The smaller / larger of two ints. diff --git a/docs/language/builtins/fn-print.md b/docs/language/builtins/fn-print.md new file mode 100644 index 00000000..f0f96104 --- /dev/null +++ b/docs/language/builtins/fn-print.md @@ -0,0 +1,12 @@ +--- +id: fn-print +name: print +category: builtins +kind: builtin +tokens: print +sig: print(x) +tip: Print an int or string, followed by a newline — for headless tests and debugging. +order: 0 +--- + +Print an int or string, followed by a newline — for headless tests and debugging. diff --git a/docs/language/builtins/fn-push.md b/docs/language/builtins/fn-push.md new file mode 100644 index 00000000..164eab50 --- /dev/null +++ b/docs/language/builtins/fn-push.md @@ -0,0 +1,12 @@ +--- +id: fn-push +name: push +category: builtins +kind: builtin +tokens: push +sig: push(slice, x) +tip: Append to a slice. +order: 6 +--- + +Append to a slice. diff --git a/docs/language/builtins/fn-quit.md b/docs/language/builtins/fn-quit.md new file mode 100644 index 00000000..8f97bc90 --- /dev/null +++ b/docs/language/builtins/fn-quit.md @@ -0,0 +1,12 @@ +--- +id: fn-quit +name: quit +category: builtins +kind: builtin +tokens: quit +sig: quit() +tip: Stop the game loop after this frame. +order: 2 +--- + +Stop the game loop after this frame. diff --git a/docs/language/builtins/fn-save.md b/docs/language/builtins/fn-save.md new file mode 100644 index 00000000..545b5808 --- /dev/null +++ b/docs/language/builtins/fn-save.md @@ -0,0 +1,12 @@ +--- +id: fn-save +name: save +category: builtins +kind: builtin +tokens: save +sig: save() +tip: Serialize the entire world — every entity, property and program var — to a snapshot in one call. +order: 3 +--- + +Serialize the entire world — every entity, property and program var — to a snapshot in one call. diff --git a/docs/language/builtins/fn-str.md b/docs/language/builtins/fn-str.md new file mode 100644 index 00000000..96f41bfc --- /dev/null +++ b/docs/language/builtins/fn-str.md @@ -0,0 +1,12 @@ +--- +id: fn-str +name: str +category: builtins +kind: builtin +tokens: str +sig: str(x) -> str +tip: Convert an int/bool/fixed to text; a string passes through. +order: 1 +--- + +Convert an int/bool/fixed to text; a string passes through. Also used by `{…}` interpolation. diff --git a/docs/language/colors/_section.md b/docs/language/colors/_section.md new file mode 100644 index 00000000..e7937c23 --- /dev/null +++ b/docs/language/colors/_section.md @@ -0,0 +1,7 @@ +--- +id: colors +title: Color — the named palette +order: 9 +--- + +Color.Name lowers to a plain 0xRRGGBB integer at compile time — no runtime cost, identical to writing the hex by hand, but readable. 221 names are built in; a custom shade is any hex literal or a named const. The full palette: diff --git a/docs/language/colors/palette.json b/docs/language/colors/palette.json new file mode 100644 index 00000000..0f37ffeb --- /dev/null +++ b/docs/language/colors/palette.json @@ -0,0 +1,944 @@ +{ + "count": 221, + "groups": [ + { + "name": "Whites", + "colors": [ + { + "name": "White", + "hex": "FFFFFF" + }, + { + "name": "Snow", + "hex": "FFFAFA" + }, + { + "name": "Ivory", + "hex": "FFFFF0" + }, + { + "name": "EggShellWhite", + "hex": "F0EAD6" + }, + { + "name": "FloralWhite", + "hex": "FFFAF0" + }, + { + "name": "SeaShell", + "hex": "FFF5EE" + }, + { + "name": "Linen", + "hex": "FAF0E6" + }, + { + "name": "AntiqueWhite", + "hex": "FAEBD7" + }, + { + "name": "OldLace", + "hex": "FDF5E6" + }, + { + "name": "Beige", + "hex": "F5F5DC" + }, + { + "name": "Cream", + "hex": "FFFDD0" + }, + { + "name": "Honeydew", + "hex": "F0FFF0" + }, + { + "name": "MintCream", + "hex": "F5FFFA" + }, + { + "name": "Azure", + "hex": "F0FFFF" + }, + { + "name": "AliceBlue", + "hex": "F0F8FF" + }, + { + "name": "GhostWhite", + "hex": "F8F8FF" + }, + { + "name": "WhiteSmoke", + "hex": "F5F5F5" + }, + { + "name": "Lavender", + "hex": "E6E6FA" + }, + { + "name": "Bone", + "hex": "E3DAC9" + }, + { + "name": "Parchment", + "hex": "F1E9D2" + } + ] + }, + { + "name": "Grays", + "colors": [ + { + "name": "Gainsboro", + "hex": "DCDCDC" + }, + { + "name": "LightGray", + "hex": "D3D3D3" + }, + { + "name": "Silver", + "hex": "C0C0C0" + }, + { + "name": "Ash", + "hex": "B2BEB5" + }, + { + "name": "DarkGray", + "hex": "A9A9A9" + }, + { + "name": "Gray", + "hex": "808080" + }, + { + "name": "DimGray", + "hex": "696969" + }, + { + "name": "Nickel", + "hex": "727472" + }, + { + "name": "Slate", + "hex": "708090" + }, + { + "name": "SlateGray", + "hex": "708090" + }, + { + "name": "LightSlateGray", + "hex": "778899" + }, + { + "name": "Gunmetal", + "hex": "2A3439" + }, + { + "name": "Charcoal", + "hex": "36454F" + }, + { + "name": "Graphite", + "hex": "1C1C1C" + }, + { + "name": "Onyx", + "hex": "353839" + }, + { + "name": "Jet", + "hex": "343434" + }, + { + "name": "Black", + "hex": "000000" + }, + { + "name": "EerieBlack", + "hex": "1B1B1B" + }, + { + "name": "RaisinBlack", + "hex": "242124" + }, + { + "name": "Ebony", + "hex": "555D50" + } + ] + }, + { + "name": "Reds", + "colors": [ + { + "name": "Red", + "hex": "FF0000" + }, + { + "name": "Crimson", + "hex": "DC143C" + }, + { + "name": "Scarlet", + "hex": "FF2400" + }, + { + "name": "Vermilion", + "hex": "E34234" + }, + { + "name": "FireBrick", + "hex": "B22222" + }, + { + "name": "Cinnabar", + "hex": "E44D2E" + }, + { + "name": "DarkRed", + "hex": "8B0000" + }, + { + "name": "Maroon", + "hex": "800000" + }, + { + "name": "Ruby", + "hex": "E0115F" + }, + { + "name": "Cardinal", + "hex": "C41E3A" + }, + { + "name": "IndianRed", + "hex": "CD5C5C" + }, + { + "name": "Rust", + "hex": "B7410E" + }, + { + "name": "Sangria", + "hex": "92000A" + }, + { + "name": "Redwood", + "hex": "A45A52" + }, + { + "name": "Cerise", + "hex": "DE3163" + }, + { + "name": "Amaranth", + "hex": "E52B50" + }, + { + "name": "Carmine", + "hex": "960018" + }, + { + "name": "Chestnut", + "hex": "954535" + }, + { + "name": "Brick", + "hex": "CB4154" + }, + { + "name": "TerraCotta", + "hex": "E2725B" + } + ] + }, + { + "name": "Pinks", + "colors": [ + { + "name": "Pink", + "hex": "FFC0CB" + }, + { + "name": "LightPink", + "hex": "FFB6C1" + }, + { + "name": "HotPink", + "hex": "FF69B4" + }, + { + "name": "DeepPink", + "hex": "FF1493" + }, + { + "name": "PaleVioletRed", + "hex": "DB7093" + }, + { + "name": "Rose", + "hex": "FF007F" + }, + { + "name": "Blush", + "hex": "DE5D83" + }, + { + "name": "Salmon", + "hex": "FA8072" + }, + { + "name": "LightSalmon", + "hex": "FFA07A" + }, + { + "name": "DarkSalmon", + "hex": "E9967A" + }, + { + "name": "Coral", + "hex": "FF7F50" + }, + { + "name": "Watermelon", + "hex": "FC6C85" + }, + { + "name": "Flamingo", + "hex": "FC8EAC" + }, + { + "name": "Bubblegum", + "hex": "FFC1CC" + }, + { + "name": "Fuchsia", + "hex": "FF00FF" + }, + { + "name": "Magenta", + "hex": "FF00FF" + }, + { + "name": "Mauve", + "hex": "E0B0FF" + }, + { + "name": "Puce", + "hex": "CC8899" + }, + { + "name": "Thistle", + "hex": "D8BFD8" + }, + { + "name": "Orchid", + "hex": "DA70D6" + } + ] + }, + { + "name": "Oranges", + "colors": [ + { + "name": "Orange", + "hex": "FFA500" + }, + { + "name": "DarkOrange", + "hex": "FF8C00" + }, + { + "name": "Tangerine", + "hex": "F28500" + }, + { + "name": "Pumpkin", + "hex": "FF7518" + }, + { + "name": "Apricot", + "hex": "FBCEB1" + }, + { + "name": "Peach", + "hex": "FFE5B4" + }, + { + "name": "Cantaloupe", + "hex": "FFA62B" + }, + { + "name": "Amber", + "hex": "FFBF00" + }, + { + "name": "Bronze", + "hex": "CD7F32" + }, + { + "name": "Copper", + "hex": "B87333" + }, + { + "name": "Marigold", + "hex": "EAA221" + }, + { + "name": "Carrot", + "hex": "ED9121" + }, + { + "name": "Persimmon", + "hex": "EC5800" + }, + { + "name": "Papaya", + "hex": "FF9E2C" + }, + { + "name": "Sunset", + "hex": "FAD6A5" + } + ] + }, + { + "name": "Yellows", + "colors": [ + { + "name": "Yellow", + "hex": "FFFF00" + }, + { + "name": "LightYellow", + "hex": "FFFFE0" + }, + { + "name": "Gold", + "hex": "FFD700" + }, + { + "name": "Goldenrod", + "hex": "DAA520" + }, + { + "name": "Lemon", + "hex": "FFF700" + }, + { + "name": "Canary", + "hex": "FFEF00" + }, + { + "name": "Mustard", + "hex": "FFDB58" + }, + { + "name": "Flax", + "hex": "EEDC82" + }, + { + "name": "Wheat", + "hex": "F5DEB3" + }, + { + "name": "Corn", + "hex": "FBEC5D" + }, + { + "name": "Dandelion", + "hex": "F0E130" + }, + { + "name": "Saffron", + "hex": "F4C430" + }, + { + "name": "Khaki", + "hex": "F0E68C" + }, + { + "name": "DarkKhaki", + "hex": "BDB76B" + }, + { + "name": "Straw", + "hex": "E4D96F" + } + ] + }, + { + "name": "Browns", + "colors": [ + { + "name": "Brown", + "hex": "8B4513" + }, + { + "name": "SaddleBrown", + "hex": "8B4513" + }, + { + "name": "Sienna", + "hex": "A0522D" + }, + { + "name": "Chocolate", + "hex": "D2691E" + }, + { + "name": "Peru", + "hex": "CD853F" + }, + { + "name": "Tan", + "hex": "D2B48C" + }, + { + "name": "BurlyWood", + "hex": "DEB887" + }, + { + "name": "Sand", + "hex": "C2B280" + }, + { + "name": "Coffee", + "hex": "6F4E37" + }, + { + "name": "Espresso", + "hex": "4B3621" + }, + { + "name": "Mahogany", + "hex": "C04000" + }, + { + "name": "Walnut", + "hex": "773F1A" + }, + { + "name": "Umber", + "hex": "635147" + }, + { + "name": "Sepia", + "hex": "704214" + }, + { + "name": "Taupe", + "hex": "483C32" + }, + { + "name": "Fawn", + "hex": "E5AA70" + }, + { + "name": "Caramel", + "hex": "C68E17" + }, + { + "name": "Cocoa", + "hex": "D2691E" + }, + { + "name": "Hazel", + "hex": "8E7618" + }, + { + "name": "Wenge", + "hex": "645452" + } + ] + }, + { + "name": "Greens", + "colors": [ + { + "name": "Green", + "hex": "008000" + }, + { + "name": "Lime", + "hex": "00FF00" + }, + { + "name": "LimeGreen", + "hex": "32CD32" + }, + { + "name": "LawnGreen", + "hex": "7CFC00" + }, + { + "name": "Chartreuse", + "hex": "7FFF00" + }, + { + "name": "GreenYellow", + "hex": "ADFF2F" + }, + { + "name": "SpringGreen", + "hex": "00FF7F" + }, + { + "name": "MintGreen", + "hex": "98FF98" + }, + { + "name": "SeaGreen", + "hex": "2E8B57" + }, + { + "name": "MediumSeaGreen", + "hex": "3CB371" + }, + { + "name": "ForestGreen", + "hex": "228B22" + }, + { + "name": "DarkGreen", + "hex": "006400" + }, + { + "name": "OliveDrab", + "hex": "6B8E23" + }, + { + "name": "Olive", + "hex": "808000" + }, + { + "name": "Moss", + "hex": "8A9A5B" + }, + { + "name": "Fern", + "hex": "4F7942" + }, + { + "name": "Emerald", + "hex": "50C878" + }, + { + "name": "Jade", + "hex": "00A86B" + }, + { + "name": "Malachite", + "hex": "0BDA51" + }, + { + "name": "Shamrock", + "hex": "009E60" + }, + { + "name": "Pistachio", + "hex": "93C572" + }, + { + "name": "Avocado", + "hex": "568203" + }, + { + "name": "Pine", + "hex": "01796F" + }, + { + "name": "Sage", + "hex": "9CAF88" + }, + { + "name": "Kelly", + "hex": "4CBB17" + }, + { + "name": "Hunter", + "hex": "355E3B" + }, + { + "name": "Basil", + "hex": "579229" + }, + { + "name": "Clover", + "hex": "2E8B57" + }, + { + "name": "Juniper", + "hex": "6D9A79" + }, + { + "name": "Neon", + "hex": "39FF14" + } + ] + }, + { + "name": "Cyans", + "colors": [ + { + "name": "Cyan", + "hex": "00FFFF" + }, + { + "name": "Aqua", + "hex": "00FFFF" + }, + { + "name": "LightCyan", + "hex": "E0FFFF" + }, + { + "name": "PaleTurquoise", + "hex": "AFEEEE" + }, + { + "name": "Aquamarine", + "hex": "7FFFD4" + }, + { + "name": "Turquoise", + "hex": "40E0D0" + }, + { + "name": "MediumTurquoise", + "hex": "48D1CC" + }, + { + "name": "DarkTurquoise", + "hex": "00CED1" + }, + { + "name": "Teal", + "hex": "008080" + }, + { + "name": "DarkCyan", + "hex": "008B8B" + }, + { + "name": "CadetBlue", + "hex": "5F9EA0" + }, + { + "name": "Lagoon", + "hex": "018E8E" + }, + { + "name": "Seafoam", + "hex": "93E9BE" + }, + { + "name": "Cerulean", + "hex": "007BA7" + }, + { + "name": "SkyBlueLight", + "hex": "80DAEB" + }, + { + "name": "Robin", + "hex": "00CCCC" + }, + { + "name": "Verdigris", + "hex": "43B3AE" + }, + { + "name": "Celadon", + "hex": "ACE1AF" + } + ] + }, + { + "name": "Blues", + "colors": [ + { + "name": "Blue", + "hex": "0000FF" + }, + { + "name": "LightBlue", + "hex": "ADD8E6" + }, + { + "name": "PowderBlue", + "hex": "B0E0E6" + }, + { + "name": "SkyBlue", + "hex": "87CEEB" + }, + { + "name": "LightSkyBlue", + "hex": "87CEFA" + }, + { + "name": "DeepSkyBlue", + "hex": "00BFFF" + }, + { + "name": "DodgerBlue", + "hex": "1E90FF" + }, + { + "name": "CornflowerBlue", + "hex": "6495ED" + }, + { + "name": "SteelBlue", + "hex": "4682B4" + }, + { + "name": "RoyalBlue", + "hex": "4169E1" + }, + { + "name": "MediumBlue", + "hex": "0000CD" + }, + { + "name": "DarkBlue", + "hex": "00008B" + }, + { + "name": "Navy", + "hex": "000080" + }, + { + "name": "MidnightBlue", + "hex": "191970" + }, + { + "name": "Cobalt", + "hex": "0047AB" + }, + { + "name": "Sapphire", + "hex": "0F52BA" + }, + { + "name": "Denim", + "hex": "1560BD" + }, + { + "name": "Indigo", + "hex": "4B0082" + }, + { + "name": "Prussian", + "hex": "003153" + }, + { + "name": "Ultramarine", + "hex": "3F00FF" + }, + { + "name": "Periwinkle", + "hex": "CCCCFF" + }, + { + "name": "Iris", + "hex": "5A4FCF" + }, + { + "name": "Glaucous", + "hex": "6082B6" + }, + { + "name": "Zaffre", + "hex": "0014A8" + }, + { + "name": "Berry", + "hex": "2E2D88" + } + ] + }, + { + "name": "Purples", + "colors": [ + { + "name": "Purple", + "hex": "800080" + }, + { + "name": "Violet", + "hex": "EE82EE" + }, + { + "name": "DarkViolet", + "hex": "9400D3" + }, + { + "name": "BlueViolet", + "hex": "8A2BE2" + }, + { + "name": "MediumPurple", + "hex": "9370DB" + }, + { + "name": "Amethyst", + "hex": "9966CC" + }, + { + "name": "Plum", + "hex": "8E4585" + }, + { + "name": "Eggplant", + "hex": "614051" + }, + { + "name": "Grape", + "hex": "6F2DA8" + }, + { + "name": "Wine", + "hex": "722F37" + }, + { + "name": "Mulberry", + "hex": "C54B8C" + }, + { + "name": "Lilac", + "hex": "C8A2C8" + }, + { + "name": "Wisteria", + "hex": "C9A0DC" + }, + { + "name": "Heliotrope", + "hex": "DF73FF" + }, + { + "name": "Byzantium", + "hex": "702963" + }, + { + "name": "Tyrian", + "hex": "66023C" + }, + { + "name": "RebeccaPurple", + "hex": "663399" + }, + { + "name": "Orchid2", + "hex": "AF69EF" + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/language/control/_section.md b/docs/language/control/_section.md new file mode 100644 index 00000000..0a4d88a7 --- /dev/null +++ b/docs/language/control/_section.md @@ -0,0 +1,7 @@ +--- +id: control +title: Control flow +order: 2 +--- + +Branches, loops, and state machines. diff --git a/docs/language/control/kw-become.md b/docs/language/control/kw-become.md new file mode 100644 index 00000000..c837082e --- /dev/null +++ b/docs/language/control/kw-become.md @@ -0,0 +1,12 @@ +--- +id: kw-become +name: become +category: control +kind: keyword +tokens: become +sig: become Name +tip: Transition: to another state of the enclosing machine, or to another scene. +order: 8 +--- + +Transition: to another state of the enclosing machine, or to another scene. diff --git a/docs/language/control/kw-for.md b/docs/language/control/kw-for.md new file mode 100644 index 00000000..6d3eec2f --- /dev/null +++ b/docs/language/control/kw-for.md @@ -0,0 +1,16 @@ +--- +id: kw-for +name: for … in +category: control +kind: keyword +tokens: for +sig: for name in a .. b { … } +tip: Range loop. +order: 2 +--- + +Range loop. Each pass binds a fresh, immutable name — it is not a variable you reuse or reassign; the range a .. b runs from a up to but not including b. + +```ludic +for gy in 0 .. GRID_H { … } +``` diff --git a/docs/language/control/kw-if.md b/docs/language/control/kw-if.md new file mode 100644 index 00000000..4957557f --- /dev/null +++ b/docs/language/control/kw-if.md @@ -0,0 +1,12 @@ +--- +id: kw-if +name: if / else +category: control +kind: keyword +tokens: if else +sig: if cond { … } else { … } +tip: A branch. +order: 0 +--- + +A branch. Conditions are plain expressions; no parentheses required. diff --git a/docs/language/control/kw-in.md b/docs/language/control/kw-in.md new file mode 100644 index 00000000..27e75a76 --- /dev/null +++ b/docs/language/control/kw-in.md @@ -0,0 +1,12 @@ +--- +id: kw-in +name: in +category: control +kind: keyword +tokens: in +sig: for x in range | query +tip: Binds the loop name to each value of a range or query. +order: 3 +--- + +Binds the loop name to each value of a range or query. diff --git a/docs/language/control/kw-machine.md b/docs/language/control/kw-machine.md new file mode 100644 index 00000000..e94773e0 --- /dev/null +++ b/docs/language/control/kw-machine.md @@ -0,0 +1,16 @@ +--- +id: kw-machine +name: machine +category: control +kind: keyword +tokens: machine +sig: machine store { state Name { … } } +tip: A state machine over an int var (or register). +order: 6 +--- + +A state machine over an int var (or register). It dispatches on the store's value. + +```ludic +machine turn_phase { state KnightMenu { … } } +``` diff --git a/docs/language/control/kw-match.md b/docs/language/control/kw-match.md new file mode 100644 index 00000000..6b4f5059 --- /dev/null +++ b/docs/language/control/kw-match.md @@ -0,0 +1,12 @@ +--- +id: kw-match +name: match / when +category: control +kind: keyword +tokens: match +sig: match x { when a, b => … } +tip: Multi-way branch on a value, matching one or more literals per arm. +order: 4 +--- + +Multi-way branch on a value, matching one or more literals per arm. diff --git a/docs/language/control/kw-state.md b/docs/language/control/kw-state.md new file mode 100644 index 00000000..e4c4a91a --- /dev/null +++ b/docs/language/control/kw-state.md @@ -0,0 +1,12 @@ +--- +id: kw-state +name: state +category: control +kind: keyword +tokens: state +sig: state Name { … } +tip: One state of a machine. +order: 7 +--- + +One state of a machine. diff --git a/docs/language/control/kw-when.md b/docs/language/control/kw-when.md new file mode 100644 index 00000000..976e723c --- /dev/null +++ b/docs/language/control/kw-when.md @@ -0,0 +1,12 @@ +--- +id: kw-when +name: when +category: control +kind: keyword +tokens: when +sig: when value => result +tip: One arm of a match. +order: 5 +--- + +One arm of a match. diff --git a/docs/language/control/kw-while.md b/docs/language/control/kw-while.md new file mode 100644 index 00000000..8a75d60f --- /dev/null +++ b/docs/language/control/kw-while.md @@ -0,0 +1,12 @@ +--- +id: kw-while +name: while +category: control +kind: keyword +tokens: while +sig: while cond { … } +tip: Loop while the condition holds. +order: 1 +--- + +Loop while the condition holds. diff --git a/docs/language/ecs/_section.md b/docs/language/ecs/_section.md new file mode 100644 index 00000000..be24fdc7 --- /dev/null +++ b/docs/language/ecs/_section.md @@ -0,0 +1,7 @@ +--- +id: ecs +title: Entities & the ECS +order: 1 +--- + +Entities are ids; properties are their data; queries walk them. diff --git a/docs/language/ecs/fn-world_get.md b/docs/language/ecs/fn-world_get.md new file mode 100644 index 00000000..9d6c8c7e --- /dev/null +++ b/docs/language/ecs/fn-world_get.md @@ -0,0 +1,12 @@ +--- +id: fn-world_get +name: world_get / world_set / world_has … +category: ecs +kind: builtin +tokens: world_get world_set world_has world_count world_spawn +sig: world_get(entity, prop, field) -> int +tip: The reflection ABI: read and write the world by numeric id, for tools and mods. +order: 8 +--- + +The reflection ABI: read and write the world by numeric id, for tools and mods. See also world_count, world_spawn. diff --git a/docs/language/ecs/kw-attach.md b/docs/language/ecs/kw-attach.md new file mode 100644 index 00000000..45b708ee --- /dev/null +++ b/docs/language/ecs/kw-attach.md @@ -0,0 +1,12 @@ +--- +id: kw-attach +name: attach +category: ecs +kind: keyword +tokens: attach +sig: attach Prop on entity { overrides } +tip: Add a property to a live entity. +order: 6 +--- + +Add a property to a live entity. diff --git a/docs/language/ecs/kw-despawn.md b/docs/language/ecs/kw-despawn.md new file mode 100644 index 00000000..16c2df58 --- /dev/null +++ b/docs/language/ecs/kw-despawn.md @@ -0,0 +1,12 @@ +--- +id: kw-despawn +name: despawn +category: ecs +kind: keyword +tokens: despawn +sig: despawn entity +tip: Remove an entity. +order: 1 +--- + +Remove an entity. Inside a query, despawn self() removes the current match. diff --git a/docs/language/ecs/kw-detach.md b/docs/language/ecs/kw-detach.md new file mode 100644 index 00000000..11b7f9e6 --- /dev/null +++ b/docs/language/ecs/kw-detach.md @@ -0,0 +1,12 @@ +--- +id: kw-detach +name: detach +category: ecs +kind: keyword +tokens: detach +sig: detach Prop on entity +tip: Remove a property from a live entity. +order: 7 +--- + +Remove a property from a live entity. diff --git a/docs/language/ecs/kw-disable.md b/docs/language/ecs/kw-disable.md new file mode 100644 index 00000000..26159377 --- /dev/null +++ b/docs/language/ecs/kw-disable.md @@ -0,0 +1,12 @@ +--- +id: kw-disable +name: disable +category: ecs +kind: keyword +tokens: disable +sig: disable Prop on entity +tip: Deactivate a property without destroying its data. +order: 5 +--- + +Deactivate a property without destroying its data. Also disable Model / a whole handler. diff --git a/docs/language/ecs/kw-enable.md b/docs/language/ecs/kw-enable.md new file mode 100644 index 00000000..8200ad53 --- /dev/null +++ b/docs/language/ecs/kw-enable.md @@ -0,0 +1,12 @@ +--- +id: kw-enable +name: enable +category: ecs +kind: keyword +tokens: enable +sig: enable Prop on entity +tip: Re-activate a disabled property; its stored values are intact. +order: 4 +--- + +Re-activate a disabled property; its stored values are intact. diff --git a/docs/language/ecs/kw-query.md b/docs/language/ecs/kw-query.md new file mode 100644 index 00000000..9c4c7db3 --- /dev/null +++ b/docs/language/ecs/kw-query.md @@ -0,0 +1,16 @@ +--- +id: kw-query +name: query +category: ecs +kind: keyword +tokens: query +sig: query [PropA, PropB, {Tag}] +tip: Match every entity that carries all listed properties. +order: 2 +--- + +Match every entity that carries all listed properties. Use it in for (a, b) in query […]; a {Tag} filters without binding. + +```ludic +for (p, s) in query [Pos, Seg] { … } +``` diff --git a/docs/language/ecs/kw-self.md b/docs/language/ecs/kw-self.md new file mode 100644 index 00000000..22d9bff3 --- /dev/null +++ b/docs/language/ecs/kw-self.md @@ -0,0 +1,12 @@ +--- +id: kw-self +name: self +category: ecs +kind: keyword +tokens: self +sig: self() +tip: The entity currently bound by the enclosing query. +order: 3 +--- + +The entity currently bound by the enclosing query. diff --git a/docs/language/ecs/kw-spawn.md b/docs/language/ecs/kw-spawn.md new file mode 100644 index 00000000..b91f2215 --- /dev/null +++ b/docs/language/ecs/kw-spawn.md @@ -0,0 +1,16 @@ +--- +id: kw-spawn +name: spawn +category: ecs +kind: keyword +tokens: spawn +sig: spawn Label { Prop { field: v }; … } +tip: Create an entity carrying the listed properties (or a model). +order: 0 +--- + +Create an entity carrying the listed properties (or a model). The label is for readability. + +```ludic +spawn Body { Seg { order: 1 }; Pos { x: 9, y: 7 } } +``` diff --git a/docs/language/events/_section.md b/docs/language/events/_section.md new file mode 100644 index 00000000..d7754036 --- /dev/null +++ b/docs/language/events/_section.md @@ -0,0 +1,7 @@ +--- +id: events +title: Events +order: 4 +--- + +Decoupled, named messages between handlers. diff --git a/docs/language/events/kw-cancel.md b/docs/language/events/kw-cancel.md new file mode 100644 index 00000000..f76f04cf --- /dev/null +++ b/docs/language/events/kw-cancel.md @@ -0,0 +1,12 @@ +--- +id: kw-cancel +name: cancel +category: events +kind: keyword +tokens: cancel +sig: cancel +tip: Inside a listener, veto a cancellable event. +order: 2 +--- + +Inside a listener, veto a cancellable event. diff --git a/docs/language/events/kw-emit.md b/docs/language/events/kw-emit.md new file mode 100644 index 00000000..85c9b26a --- /dev/null +++ b/docs/language/events/kw-emit.md @@ -0,0 +1,12 @@ +--- +id: kw-emit +name: emit +category: events +kind: keyword +tokens: emit +sig: emit Name(field: v) +tip: Fire an event, invoking its listeners. +order: 1 +--- + +Fire an event, invoking its listeners. As an expression it yields a cancellable event's cancelled flag. diff --git a/docs/language/events/kw-event.md b/docs/language/events/kw-event.md new file mode 100644 index 00000000..b22631f7 --- /dev/null +++ b/docs/language/events/kw-event.md @@ -0,0 +1,12 @@ +--- +id: kw-event +name: event +category: events +kind: keyword +tokens: event +sig: event Name { field: T = default } +tip: Declare a public event payload. +order: 0 +--- + +Declare a public event payload. diff --git a/docs/language/input/_section.md b/docs/language/input/_section.md new file mode 100644 index 00000000..82e97407 --- /dev/null +++ b/docs/language/input/_section.md @@ -0,0 +1,7 @@ +--- +id: input +title: Input +order: 6 +--- + +Reading the keyboard. diff --git a/docs/language/input/input-key.md b/docs/language/input/input-key.md new file mode 100644 index 00000000..7a3d6e77 --- /dev/null +++ b/docs/language/input/input-key.md @@ -0,0 +1,17 @@ +--- +id: input-key +name: Input.key +category: input +kind: namespace-method +tokens: Input.key +sig: Input.key() -> int +tip: The key pressed this frame, as a character code (0 when nothing is pressed). +order: 0 +--- + +The key pressed this frame, as a character code (0 when nothing is pressed). Compare against character literals like 'w'. + +```ludic +let k = Input.key() +if k == 'w' { … } +``` diff --git a/docs/language/map/_section.md b/docs/language/map/_section.md new file mode 100644 index 00000000..4a81ff58 --- /dev/null +++ b/docs/language/map/_section.md @@ -0,0 +1,7 @@ +--- +id: map +title: Map — tilemap +order: 8 +--- + +A character grid the game paints and reads. diff --git a/docs/language/map/map-row.md b/docs/language/map/map-row.md new file mode 100644 index 00000000..a93cc3aa --- /dev/null +++ b/docs/language/map/map-row.md @@ -0,0 +1,16 @@ +--- +id: map-row +name: Map.row +category: map +kind: namespace-method +tokens: Map.row +sig: Map.row(y, cells) +tip: Fill one row of the tilemap from a string of tile characters. +order: 1 +--- + +Fill one row of the tilemap from a string of tile characters. + +```ludic +Map.row(y: 0, cells: "####......####") +``` diff --git a/docs/language/map/map-size.md b/docs/language/map/map-size.md new file mode 100644 index 00000000..f2487586 --- /dev/null +++ b/docs/language/map/map-size.md @@ -0,0 +1,12 @@ +--- +id: map-size +name: Map.size +category: map +kind: namespace-method +tokens: Map.size +sig: Map.size(width, height) +tip: Set the tilemap dimensions in cells. +order: 0 +--- + +Set the tilemap dimensions in cells. diff --git a/docs/language/map/map-tile.md b/docs/language/map/map-tile.md new file mode 100644 index 00000000..96919691 --- /dev/null +++ b/docs/language/map/map-tile.md @@ -0,0 +1,12 @@ +--- +id: map-tile +name: Map.tile +category: map +kind: namespace-method +tokens: Map.tile +sig: Map.tile(x, y) -> int +tip: Read the tile character at a cell. +order: 2 +--- + +Read the tile character at a cell. Out-of-bounds reads answer '#', so the edge of the world is a wall for free. diff --git a/docs/language/operators/_section.md b/docs/language/operators/_section.md new file mode 100644 index 00000000..971a7c33 --- /dev/null +++ b/docs/language/operators/_section.md @@ -0,0 +1,7 @@ +--- +id: operators +title: Operators & tokens +order: 11 +--- + +The symbols the grammar recognizes. diff --git a/docs/language/operators/op-access.md b/docs/language/operators/op-access.md new file mode 100644 index 00000000..6d4b4205 --- /dev/null +++ b/docs/language/operators/op-access.md @@ -0,0 +1,11 @@ +--- +id: op-access +name: Member & index +category: operators +kind: operator +sig: x.field buf[i] s[a..b] +tip: Field/method access, element index, and string slice (a fresh substring). +order: 6 +--- + +Field/method access, element index, and string slice (a fresh substring). diff --git a/docs/language/operators/op-arith.md b/docs/language/operators/op-arith.md new file mode 100644 index 00000000..e54141c1 --- /dev/null +++ b/docs/language/operators/op-arith.md @@ -0,0 +1,11 @@ +--- +id: op-arith +name: Arithmetic +category: operators +kind: operator +sig: + - * / % +tip: Add, subtract, multiply, integer-divide, remainder. +order: 0 +--- + +Add, subtract, multiply, integer-divide, remainder. On fixed values the same symbols do fixed-point math. diff --git a/docs/language/operators/op-assign.md b/docs/language/operators/op-assign.md new file mode 100644 index 00000000..99be2a6d --- /dev/null +++ b/docs/language/operators/op-assign.md @@ -0,0 +1,11 @@ +--- +id: op-assign +name: Assignment +category: operators +kind: operator +sig: name = value +tip: Assign to a var, a field, or an element. +order: 5 +--- + +Assign to a var, a field, or an element. Not an expression. diff --git a/docs/language/operators/op-bitwise.md b/docs/language/operators/op-bitwise.md new file mode 100644 index 00000000..293fe2cb --- /dev/null +++ b/docs/language/operators/op-bitwise.md @@ -0,0 +1,11 @@ +--- +id: op-bitwise +name: Bitwise +category: operators +kind: operator +sig: & | ^ ~ << >> +tip: And, or, xor, not, shift left/right. +order: 3 +--- + +And, or, xor, not, shift left/right. Shifts and & bind like *; |/^ bind like + — tighter than comparison, so flags & MASK == 0 needs no parentheses. diff --git a/docs/language/operators/op-comment.md b/docs/language/operators/op-comment.md new file mode 100644 index 00000000..b3750804 --- /dev/null +++ b/docs/language/operators/op-comment.md @@ -0,0 +1,11 @@ +--- +id: op-comment +name: Comment +category: operators +kind: operator +sig: # to end of line +tip: Everything after # on a line is a comment. +order: 9 +--- + +Everything after # on a line is a comment. diff --git a/docs/language/operators/op-compare.md b/docs/language/operators/op-compare.md new file mode 100644 index 00000000..b9da4eda --- /dev/null +++ b/docs/language/operators/op-compare.md @@ -0,0 +1,11 @@ +--- +id: op-compare +name: Comparison +category: operators +kind: operator +sig: == != < <= > >= +tip: Yield a bool. +order: 1 +--- + +Yield a bool. On strings, == compares contents. diff --git a/docs/language/operators/op-interp.md b/docs/language/operators/op-interp.md new file mode 100644 index 00000000..c7180324 --- /dev/null +++ b/docs/language/operators/op-interp.md @@ -0,0 +1,15 @@ +--- +id: op-interp +name: String interpolation +category: operators +kind: operator +sig: `text {expr} more` +tip: A backtick string with {expr} holes, each stringified and concatenated. +order: 7 +--- + +A backtick string with {expr} holes, each stringified and concatenated. {{ and }} are literal braces. + +```ludic +print(`score: {score}`) +``` diff --git a/docs/language/operators/op-literals.md b/docs/language/operators/op-literals.md new file mode 100644 index 00000000..cb6bc468 --- /dev/null +++ b/docs/language/operators/op-literals.md @@ -0,0 +1,11 @@ +--- +id: op-literals +name: Literals +category: operators +kind: operator +sig: 42 0x1E90FF 'w' "text" true null +tip: Decimal and hex ints (hex is how colors are written), a character code in single quotes, a string in double quotes, booleans, and the null pointer. +order: 8 +--- + +Decimal and hex ints (hex is how colors are written), a character code in single quotes, a string in double quotes, booleans, and the null pointer. diff --git a/docs/language/operators/op-logical.md b/docs/language/operators/op-logical.md new file mode 100644 index 00000000..63e392be --- /dev/null +++ b/docs/language/operators/op-logical.md @@ -0,0 +1,15 @@ +--- +id: op-logical +name: Logical +category: operators +kind: operator +sig: and or not +tip: Boolean combinators — words, not symbols. +order: 2 +--- + +Boolean combinators — words, not symbols. + +```ludic +if k != 0 and mode == 0 { … } +``` diff --git a/docs/language/operators/op-range.md b/docs/language/operators/op-range.md new file mode 100644 index 00000000..9b55b1df --- /dev/null +++ b/docs/language/operators/op-range.md @@ -0,0 +1,11 @@ +--- +id: op-range +name: Range +category: operators +kind: operator +sig: a .. b +tip: A half-open range for for loops: a up to but not including b. +order: 4 +--- + +A half-open range for for loops: a up to but not including b. diff --git a/docs/language/random/_section.md b/docs/language/random/_section.md new file mode 100644 index 00000000..03258f7e --- /dev/null +++ b/docs/language/random/_section.md @@ -0,0 +1,7 @@ +--- +id: random +title: Random +order: 7 +--- + +A seeded, deterministic RNG — same seed, same sequence, every run and every platform. diff --git a/docs/language/random/random-chance.md b/docs/language/random/random-chance.md new file mode 100644 index 00000000..391935ae --- /dev/null +++ b/docs/language/random/random-chance.md @@ -0,0 +1,12 @@ +--- +id: random-chance +name: Random.chance +category: random +kind: namespace-method +tokens: Random.chance +sig: Random.chance(percent) -> bool +tip: True with the given percent probability. +order: 1 +--- + +True with the given percent probability. diff --git a/docs/language/random/random-range.md b/docs/language/random/random-range.md new file mode 100644 index 00000000..4015e332 --- /dev/null +++ b/docs/language/random/random-range.md @@ -0,0 +1,16 @@ +--- +id: random-range +name: Random.range +category: random +kind: namespace-method +tokens: Random.range +sig: Random.range(low, high) -> int +tip: A random integer in the inclusive range [low, high]. +order: 0 +--- + +A random integer in the inclusive range [low, high]. + +```ludic +food_x = Random.range(low: 0, high: GRID_W - 1) +``` diff --git a/docs/language/random/random-seed.md b/docs/language/random/random-seed.md new file mode 100644 index 00000000..ddf87af1 --- /dev/null +++ b/docs/language/random/random-seed.md @@ -0,0 +1,12 @@ +--- +id: random-seed +name: Random.seed +category: random +kind: namespace-method +tokens: Random.seed +sig: Random.seed(value) +tip: Seed the RNG. +order: 2 +--- + +Seed the RNG. Seeding with the same value makes runs reproducible. diff --git a/docs/language/scenes/_section.md b/docs/language/scenes/_section.md new file mode 100644 index 00000000..2346e963 --- /dev/null +++ b/docs/language/scenes/_section.md @@ -0,0 +1,7 @@ +--- +id: scenes +title: Scenes & layers +order: 3 +--- + +One active scene at a time, each grouping handlers into layers. diff --git a/docs/language/scenes/kw-enter.md b/docs/language/scenes/kw-enter.md new file mode 100644 index 00000000..f7abdcb1 --- /dev/null +++ b/docs/language/scenes/kw-enter.md @@ -0,0 +1,12 @@ +--- +id: kw-enter +name: enter +category: scenes +kind: keyword +tokens: enter +sig: on enter { … } +tip: The scene-entry hook. +order: 3 +--- + +The scene-entry hook. diff --git a/docs/language/scenes/kw-exit.md b/docs/language/scenes/kw-exit.md new file mode 100644 index 00000000..ef41b48f --- /dev/null +++ b/docs/language/scenes/kw-exit.md @@ -0,0 +1,12 @@ +--- +id: kw-exit +name: exit +category: scenes +kind: keyword +tokens: exit +sig: on exit { … } +tip: The scene-exit hook. +order: 4 +--- + +The scene-exit hook. diff --git a/docs/language/scenes/kw-layer.md b/docs/language/scenes/kw-layer.md new file mode 100644 index 00000000..b4ec299f --- /dev/null +++ b/docs/language/scenes/kw-layer.md @@ -0,0 +1,12 @@ +--- +id: kw-layer +name: layer +category: scenes +kind: keyword +tokens: layer +sig: layer Name { handlers } +tip: A group of handlers inside a scene; layers render in declaration order. +order: 1 +--- + +A group of handlers inside a scene; layers render in declaration order. diff --git a/docs/language/scenes/kw-on.md b/docs/language/scenes/kw-on.md new file mode 100644 index 00000000..5853ed20 --- /dev/null +++ b/docs/language/scenes/kw-on.md @@ -0,0 +1,12 @@ +--- +id: kw-on +name: on enter / on exit +category: scenes +kind: keyword +tokens: on +sig: on enter { … } on exit { … } +tip: Hooks that fire when a scene becomes active or is left. +order: 2 +--- + +Hooks that fire when a scene becomes active or is left. diff --git a/docs/language/scenes/kw-scene.md b/docs/language/scenes/kw-scene.md new file mode 100644 index 00000000..ee9a6106 --- /dev/null +++ b/docs/language/scenes/kw-scene.md @@ -0,0 +1,16 @@ +--- +id: kw-scene +name: scene +category: scenes +kind: keyword +tokens: scene +sig: scene Name [start] { on enter{} on exit{} layer … } +tip: A mutually-exclusive game state. +order: 0 +--- + +A mutually-exclusive game state. Mark one start. + +```ludic +scene Title start { on enter { … } layer Main { … } } +``` diff --git a/docs/language/screen/_section.md b/docs/language/screen/_section.md new file mode 100644 index 00000000..8c8dd620 --- /dev/null +++ b/docs/language/screen/_section.md @@ -0,0 +1,7 @@ +--- +id: screen +title: Screen — drawing +order: 5 +--- + +The 2D drawing surface. Every call takes named arguments; draw during the Render phase, then Screen.show(). diff --git a/docs/language/screen/screen-clear.md b/docs/language/screen/screen-clear.md new file mode 100644 index 00000000..99ec0ae2 --- /dev/null +++ b/docs/language/screen/screen-clear.md @@ -0,0 +1,16 @@ +--- +id: screen-clear +name: Screen.clear +category: screen +kind: namespace-method +tokens: Screen.clear +sig: Screen.clear(color) +tip: Fill the entire screen with one color. +order: 0 +--- + +Fill the entire screen with one color. + +```ludic +Screen.clear(Color.MidnightBlue) +``` diff --git a/docs/language/screen/screen-draw_number.md b/docs/language/screen/screen-draw_number.md new file mode 100644 index 00000000..5ab84bcc --- /dev/null +++ b/docs/language/screen/screen-draw_number.md @@ -0,0 +1,16 @@ +--- +id: screen-draw_number +name: Screen.draw_number +category: screen +kind: namespace-method +tokens: Screen.draw_number +sig: Screen.draw_number(x, y, value, color, scale) +tip: Draw an integer — no allocation, no string conversion. +order: 5 +--- + +Draw an integer — no allocation, no string conversion. + +```ludic +Screen.draw_number(x: 52, y: 4, value: score, color: Color.Gold, scale: 1) +``` diff --git a/docs/language/screen/screen-draw_rectangle.md b/docs/language/screen/screen-draw_rectangle.md new file mode 100644 index 00000000..eb1f9b0f --- /dev/null +++ b/docs/language/screen/screen-draw_rectangle.md @@ -0,0 +1,12 @@ +--- +id: screen-draw_rectangle +name: Screen.draw_rectangle +category: screen +kind: namespace-method +tokens: Screen.draw_rectangle +sig: Screen.draw_rectangle(x, y, width, height, color) +tip: Draw a one-pixel rectangle outline. +order: 2 +--- + +Draw a one-pixel rectangle outline. diff --git a/docs/language/screen/screen-draw_text.md b/docs/language/screen/screen-draw_text.md new file mode 100644 index 00000000..39a32223 --- /dev/null +++ b/docs/language/screen/screen-draw_text.md @@ -0,0 +1,16 @@ +--- +id: screen-draw_text +name: Screen.draw_text +category: screen +kind: namespace-method +tokens: Screen.draw_text +sig: Screen.draw_text(x, y, text, color, scale) +tip: Draw a string with the built-in font at an integer scale. +order: 4 +--- + +Draw a string with the built-in font at an integer scale. + +```ludic +Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1) +``` diff --git a/docs/language/screen/screen-fill_rectangle.md b/docs/language/screen/screen-fill_rectangle.md new file mode 100644 index 00000000..e7fd2423 --- /dev/null +++ b/docs/language/screen/screen-fill_rectangle.md @@ -0,0 +1,16 @@ +--- +id: screen-fill_rectangle +name: Screen.fill_rectangle +category: screen +kind: namespace-method +tokens: Screen.fill_rectangle +sig: Screen.fill_rectangle(x, y, width, height, color) +tip: Draw a filled rectangle. +order: 1 +--- + +Draw a filled rectangle. + +```ludic +Screen.fill_rectangle(x: 8, y: 8, width: 16, height: 16, color: Color.Crimson) +``` diff --git a/docs/language/screen/screen-height.md b/docs/language/screen/screen-height.md new file mode 100644 index 00000000..f795b9fe --- /dev/null +++ b/docs/language/screen/screen-height.md @@ -0,0 +1,12 @@ +--- +id: screen-height +name: Screen.height +category: screen +kind: namespace-method +tokens: Screen.height +sig: Screen.height() -> int +tip: Framebuffer height in pixels. +order: 8 +--- + +Framebuffer height in pixels. diff --git a/docs/language/screen/screen-put_pixel.md b/docs/language/screen/screen-put_pixel.md new file mode 100644 index 00000000..4aca1265 --- /dev/null +++ b/docs/language/screen/screen-put_pixel.md @@ -0,0 +1,12 @@ +--- +id: screen-put_pixel +name: Screen.put_pixel +category: screen +kind: namespace-method +tokens: Screen.put_pixel +sig: Screen.put_pixel(x, y, color) +tip: Set a single pixel. +order: 3 +--- + +Set a single pixel. diff --git a/docs/language/screen/screen-show.md b/docs/language/screen/screen-show.md new file mode 100644 index 00000000..3f2c7be1 --- /dev/null +++ b/docs/language/screen/screen-show.md @@ -0,0 +1,16 @@ +--- +id: screen-show +name: Screen.show +category: screen +kind: namespace-method +tokens: Screen.show +sig: Screen.show() +tip: Present the finished frame — copy everything you have drawn to the window. +order: 6 +--- + +Present the finished frame — copy everything you have drawn to the window. Call it once, last, in your Render handler. + +```ludic +Screen.show() +``` diff --git a/docs/language/screen/screen-status.md b/docs/language/screen/screen-status.md new file mode 100644 index 00000000..b4da7f82 --- /dev/null +++ b/docs/language/screen/screen-status.md @@ -0,0 +1,12 @@ +--- +id: screen-status +name: Screen.status +category: screen +kind: namespace-method +tokens: Screen.status +sig: Screen.status(text) +tip: Set the persistent one-line status/HUD string. +order: 9 +--- + +Set the persistent one-line status/HUD string. diff --git a/docs/language/screen/screen-width.md b/docs/language/screen/screen-width.md new file mode 100644 index 00000000..7d3725a5 --- /dev/null +++ b/docs/language/screen/screen-width.md @@ -0,0 +1,12 @@ +--- +id: screen-width +name: Screen.width +category: screen +kind: namespace-method +tokens: Screen.width +sig: Screen.width() -> int +tip: Framebuffer width in pixels. +order: 7 +--- + +Framebuffer width in pixels. diff --git a/docs/language/structure/_section.md b/docs/language/structure/_section.md new file mode 100644 index 00000000..ea5e2a90 --- /dev/null +++ b/docs/language/structure/_section.md @@ -0,0 +1,7 @@ +--- +id: structure +title: Program structure +order: 0 +--- + +The shape of a Ludic program: one program block holding declarations. diff --git a/docs/language/structure/kw-const.md b/docs/language/structure/kw-const.md new file mode 100644 index 00000000..c0476c4e --- /dev/null +++ b/docs/language/structure/kw-const.md @@ -0,0 +1,12 @@ +--- +id: kw-const +name: const +category: structure +kind: keyword +tokens: const +sig: const NAME: T = value +tip: A compile-time constant. +order: 5 +--- + +A compile-time constant. Folds directly into the code — no storage, no cost. diff --git a/docs/language/structure/kw-extern.md b/docs/language/structure/kw-extern.md new file mode 100644 index 00000000..e3250e86 --- /dev/null +++ b/docs/language/structure/kw-extern.md @@ -0,0 +1,12 @@ +--- +id: kw-extern +name: extern +category: structure +kind: keyword +tokens: extern +sig: extern fn name(a: T) -> R = "symbol" +tip: Bind a name to an external C-ABI symbol — the seam for platform and library calls. +order: 12 +--- + +Bind a name to an external C-ABI symbol — the seam for platform and library calls. diff --git a/docs/language/structure/kw-fn.md b/docs/language/structure/kw-fn.md new file mode 100644 index 00000000..296a7d40 --- /dev/null +++ b/docs/language/structure/kw-fn.md @@ -0,0 +1,12 @@ +--- +id: kw-fn +name: fn +category: structure +kind: keyword +tokens: fn +sig: fn name(a: T, b: T) -> R { … } +tip: A function. +order: 8 +--- + +A function. Call it positionally or with named arguments: name(a: 1, b: 2). diff --git a/docs/language/structure/kw-handler.md b/docs/language/structure/kw-handler.md new file mode 100644 index 00000000..ca8143d5 --- /dev/null +++ b/docs/language/structure/kw-handler.md @@ -0,0 +1,16 @@ +--- +id: kw-handler +name: handler +category: structure +kind: keyword +tokens: handler +sig: handler Name phase P { … } +tip: A block of code the engine runs every frame during phase P. +order: 3 +--- + +A block of code the engine runs every frame during phase P. With a query, the body runs once per matching entity. + +```ludic +handler Move phase Update { … } +``` diff --git a/docs/language/structure/kw-import.md b/docs/language/structure/kw-import.md new file mode 100644 index 00000000..308f1641 --- /dev/null +++ b/docs/language/structure/kw-import.md @@ -0,0 +1,12 @@ +--- +id: kw-import +name: import +category: structure +kind: keyword +tokens: import +sig: import "file.ludic" +tip: Splice another Ludic file into this program. +order: 10 +--- + +Splice another Ludic file into this program. Paths resolve relative to the importer; re-imports are free. diff --git a/docs/language/structure/kw-let.md b/docs/language/structure/kw-let.md new file mode 100644 index 00000000..1d0ea24c --- /dev/null +++ b/docs/language/structure/kw-let.md @@ -0,0 +1,12 @@ +--- +id: kw-let +name: let +category: structure +kind: keyword +tokens: let +sig: let name = value +tip: An immutable binding, scoped to the block it appears in. +order: 7 +--- + +An immutable binding, scoped to the block it appears in. diff --git a/docs/language/structure/kw-model.md b/docs/language/structure/kw-model.md new file mode 100644 index 00000000..8f079e70 --- /dev/null +++ b/docs/language/structure/kw-model.md @@ -0,0 +1,16 @@ +--- +id: kw-model +name: model +category: structure +kind: keyword +tokens: model +sig: model Name { PropA, PropB, … } +tip: A named bundle of properties, so an entity that always travels together is spawned by one name. +order: 2 +--- + +A named bundle of properties, so an entity that always travels together is spawned by one name. + +```ludic +model Player { Health, Shield } +``` diff --git a/docs/language/structure/kw-module.md b/docs/language/structure/kw-module.md new file mode 100644 index 00000000..4aec9649 --- /dev/null +++ b/docs/language/structure/kw-module.md @@ -0,0 +1,12 @@ +--- +id: kw-module +name: module +category: structure +kind: keyword +tokens: module +sig: module Name { @export fn … } +tip: Build a shared library of plain C-ABI symbols instead of an executable. +order: 11 +--- + +Build a shared library of plain C-ABI symbols instead of an executable. diff --git a/docs/language/structure/kw-phase.md b/docs/language/structure/kw-phase.md new file mode 100644 index 00000000..99f1ab8b --- /dev/null +++ b/docs/language/structure/kw-phase.md @@ -0,0 +1,12 @@ +--- +id: kw-phase +name: phase +category: structure +kind: phase +tokens: Start Input Update FixedUpdate Render +sig: phase Start | Input | Update | FixedUpdate | Render +tip: When a handler runs. +order: 4 +--- + +When a handler runs. Start once at boot; Input reads the keyboard; Update is the per-frame step; FixedUpdate is the deterministic fixed-step; Render draws the frame. diff --git a/docs/language/structure/kw-program.md b/docs/language/structure/kw-program.md new file mode 100644 index 00000000..ab575b19 --- /dev/null +++ b/docs/language/structure/kw-program.md @@ -0,0 +1,12 @@ +--- +id: kw-program +name: program +category: structure +kind: keyword +tokens: program +sig: program Name { … } +tip: The top-level unit. +order: 0 +--- + +The top-level unit. A program compiles to one native game; everything else lives inside it. diff --git a/docs/language/structure/kw-property.md b/docs/language/structure/kw-property.md new file mode 100644 index 00000000..6c86f35b --- /dev/null +++ b/docs/language/structure/kw-property.md @@ -0,0 +1,16 @@ +--- +id: kw-property +name: property +category: structure +kind: keyword +tokens: property +sig: property Name { field: T = default, … } +tip: A component: a named record of fields an entity can carry. +order: 1 +--- + +A component: a named record of fields an entity can carry. Fields have a type and a default. + +```ludic +property Pos { x: int = 0, y: int = 0 } +``` diff --git a/docs/language/structure/kw-return.md b/docs/language/structure/kw-return.md new file mode 100644 index 00000000..15755e6f --- /dev/null +++ b/docs/language/structure/kw-return.md @@ -0,0 +1,12 @@ +--- +id: kw-return +name: return +category: structure +kind: keyword +tokens: return +sig: return value +tip: Return from a function. +order: 9 +--- + +Return from a function. diff --git a/docs/language/structure/kw-var.md b/docs/language/structure/kw-var.md new file mode 100644 index 00000000..8481a059 --- /dev/null +++ b/docs/language/structure/kw-var.md @@ -0,0 +1,16 @@ +--- +id: kw-var +name: var +category: structure +kind: keyword +tokens: var +sig: var name: T = value +tip: A mutable binding. +order: 6 +--- + +A mutable binding. At program scope it is your game's persistent, named state — the modern replacement for numeric registers. + +```ludic +var score: int = 0 +``` diff --git a/docs/language/types/_section.md b/docs/language/types/_section.md new file mode 100644 index 00000000..05af7757 --- /dev/null +++ b/docs/language/types/_section.md @@ -0,0 +1,7 @@ +--- +id: types +title: Types +order: 10 +--- + +Ludic is statically typed; most code uses just int. diff --git a/docs/language/types/type-bool.md b/docs/language/types/type-bool.md new file mode 100644 index 00000000..742ed671 --- /dev/null +++ b/docs/language/types/type-bool.md @@ -0,0 +1,12 @@ +--- +id: type-bool +name: bool +category: types +kind: type +tokens: bool +sig: bool +tip: A truth value: the result of comparisons and and/or/not. +order: 2 +--- + +A truth value: the result of comparisons and and/or/not. diff --git a/docs/language/types/type-byte.md b/docs/language/types/type-byte.md new file mode 100644 index 00000000..7b41a0a2 --- /dev/null +++ b/docs/language/types/type-byte.md @@ -0,0 +1,12 @@ +--- +id: type-byte +name: byte +category: types +kind: type +tokens: byte +sig: byte +tip: A single byte, as read from a ptr index. +order: 7 +--- + +A single byte, as read from a ptr index. diff --git a/docs/language/types/type-entity.md b/docs/language/types/type-entity.md new file mode 100644 index 00000000..29a79ce6 --- /dev/null +++ b/docs/language/types/type-entity.md @@ -0,0 +1,12 @@ +--- +id: type-entity +name: entity +category: types +kind: type +tokens: entity +sig: entity +tip: An entity id, as returned by self(). +order: 4 +--- + +An entity id, as returned by self(). diff --git a/docs/language/types/type-fixed.md b/docs/language/types/type-fixed.md new file mode 100644 index 00000000..f386c28a --- /dev/null +++ b/docs/language/types/type-fixed.md @@ -0,0 +1,12 @@ +--- +id: type-fixed +name: fixed +category: types +kind: type +tokens: fixed +sig: fixed +tip: Q16.16 fixed-point — deterministic fractional math. +order: 1 +--- + +Q16.16 fixed-point — deterministic fractional math. fx(n) lifts an int in; flr(x) takes the floor back out. diff --git a/docs/language/types/type-int.md b/docs/language/types/type-int.md new file mode 100644 index 00000000..40d6d6b8 --- /dev/null +++ b/docs/language/types/type-int.md @@ -0,0 +1,12 @@ +--- +id: type-int +name: int +category: types +kind: type +tokens: int +sig: int +tip: A 32-bit signed integer — the default numeric type, and how colors, keys and tiles are carried. +order: 0 +--- + +A 32-bit signed integer — the default numeric type, and how colors, keys and tiles are carried. diff --git a/docs/language/types/type-ptr.md b/docs/language/types/type-ptr.md new file mode 100644 index 00000000..f3ce20f0 --- /dev/null +++ b/docs/language/types/type-ptr.md @@ -0,0 +1,12 @@ +--- +id: type-ptr +name: ptr +category: types +kind: type +tokens: ptr +sig: ptr +tip: A raw byte buffer (see bytes(n)). +order: 5 +--- + +A raw byte buffer (see bytes(n)). diff --git a/docs/language/types/type-slices.md b/docs/language/types/type-slices.md new file mode 100644 index 00000000..3056b260 --- /dev/null +++ b/docs/language/types/type-slices.md @@ -0,0 +1,11 @@ +--- +id: type-slices +name: []T (slices) +category: types +kind: type +sig: []T +tip: A growable slice of T. +order: 8 +--- + +A growable slice of T. new []T makes one; push(s, x) appends; len(s) counts; s[i] indexes. diff --git a/docs/language/types/type-str.md b/docs/language/types/type-str.md new file mode 100644 index 00000000..ce9dc9a3 --- /dev/null +++ b/docs/language/types/type-str.md @@ -0,0 +1,12 @@ +--- +id: type-str +name: str +category: types +kind: type +tokens: str +sig: str +tip: An immutable string. +order: 3 +--- + +An immutable string. Index for a character code; slice with s[a..b]; interpolate with `text {expr}`. diff --git a/docs/language/types/type-words.md b/docs/language/types/type-words.md new file mode 100644 index 00000000..a0d1fa9a --- /dev/null +++ b/docs/language/types/type-words.md @@ -0,0 +1,12 @@ +--- +id: type-words +name: words +category: types +kind: type +tokens: words +sig: words +tip: A raw buffer of 32-bit words (see words(n)). +order: 6 +--- + +A raw buffer of 32-bit words (see words(n)). diff --git a/docs/site/site.json b/docs/site/site.json new file mode 100644 index 00000000..a358652a --- /dev/null +++ b/docs/site/site.json @@ -0,0 +1,104 @@ +{ + "brand": "Ludic", + "tagline": "The opinionated, compiled language for 2D games.", + "repo_url": "https://git.workshopsoft.io/workshopsoft/ludic", + "meta": { + "title": "Ludic — the opinionated game language", + "description": "Ludic is an opinionated, compiled language for 2D games. The entity system, rendering, named colors, input, deterministic math and save/load are part of the language — so you build a game, not a framework.", + "og_title": "Ludic — the opinionated game language", + "og_description": "An opinionated, compiled language for 2D games. The ECS, rendering, colors, input and save/load are built in — build a game, not a framework." + }, + "nav_links": [ + { "label": "Features", "href": "#features" }, + { "label": "Examples", "href": "#showcase" }, + { "label": "Get started", "href": "#start" }, + { "label": "API Reference", "href": "api.html" } + ], + "hero": { + "pill": "Opinionated · Batteries-included · No framework", + "title_pre": "Build a game,", + "title_accent": "not a framework.", + "lead": "Ludic is an opinionated, compiled language for 2D games. The entity system, drawing, named colors, input, deterministic math and save/load aren't libraries you wire up — they're part of the language. You write the game; there's nothing to assemble first.", + "snippet": "docs/site/snippets/hero.ludic", + "snippet_name": "hello.ludic", + "primary_cta": { "label": "Get started →", "href": "#start" }, + "secondary_cta": { "label": "API Reference", "href": "api.html" }, + "pipeline": [".ludic", "ludicc", "optimized IR", "native binary"], + "pipeline_note": "One step from your code to a native binary. No engine to install, no interpreter, no runtime to ship alongside it — the game is the executable." + }, + "features": { + "kicker": "Why Ludic", + "title": "A language shaped around the game.", + "intro": "The things you normally bolt on — an entity-component system, a renderer, a deterministic clock, save/load — are primitives of the language itself. One opinionated way to do each, so there's little to decide and nothing to wire up.", + "cards": [ + { "icon": "🧩", "title": "ECS in the syntax", "html": "property, model, and handler are keywords. Query entities with query [A, B, {Tag}] and iterate matches directly — no framework to wire up." }, + { "icon": "⏱️", "title": "Deterministic runtime", "html": "Q16.16 fixed-point math and a seeded RNG mean the same inputs produce the same frame — byte-for-byte — every run. Ideal for replays and lockstep netcode." }, + { "icon": "🎨", "title": "Drawing & named colors", "html": "The Screen API draws rectangles, text and pixels with named arguments; Color.Crimson and 220 more names read like English and cost nothing at runtime." }, + { "icon": "📦", "title": "One self-contained binary", "html": "A game compiles to a single native executable — no engine to install, no interpreter, no runtime shipped beside it. Build, and run the file." }, + { "icon": "💾", "title": "Snapshot save/load", "html": "save() and load() serialize the entire ECS world — every entity, property and program var — in one call." }, + { "icon": "🎬", "title": "Scenes & state machines", "html": "scene/layer/become model mutually-exclusive game states with enter/exit hooks; match/machine/state handle dispatch and per-entity FSMs." }, + { "icon": "🖼️", "title": "Retained UI & assets", "html": "Declare a widget tree as data with ui — panels, labels, buttons, 9-slice skins, keyboard focus. Sprites decode from PNG at runtime; text is real TrueType." }, + { "icon": "🔌", "title": "Modules & native calls", "html": "module + @export fn builds a shared library of plain native symbols; extern fn … = \"symbol\" reaches out to any native library when you need the platform." } + ] + }, + "showcase": { + "kicker": "Show, don't tell", + "title": "Real programs, one toolchain.", + "intro": "The same ludicc that builds a JRPG builds a from-scratch Snake and a scene demo. Nothing is hardcoded to a genre — and every token below links into the reference.", + "samples": [ + { "label": "snake", "name": "snake.ludic", "file": "docs/site/snippets/snake.ludic", + "note": "A complete Snake — grid, growth, food, game-over — from primitives. State is named vars, colors are named, and every draw call says what each argument is." }, + { "label": "scenes", "name": "scenes.ludic", "file": "docs/site/snippets/scenes.ludic", + "note": "One active scene at a time. `become` runs the old scene's on-exit and the new one's on-enter; layers draw in declaration order. State is a plain named var." }, + { "label": "lifecycle", "name": "toggle.ludic", "file": "docs/site/snippets/lifecycle.ludic", + "note": "Enable/disable at three scopes — entity, model, handler. Disabling never destroys data: a property's values persist, so a later enable restores them." }, + { "label": "hello", "name": "hello.ludic", "file": "docs/site/snippets/hero.ludic", + "note": "The smallest program that exercises the whole pipeline: properties, a spawn, a queried handler, and a render/quit." } + ] + }, + "philosophy": { + "kicker": "The philosophy", + "title": "No framework. No glue. Just the game.", + "paras": [ + "Most game code is plumbing — registering systems, wiring a renderer, threading state through a framework. Ludic makes those decisions for you and bakes them into the language, so the code you write is the game's actual logic.", + "Opinionated on purpose: one clear way to spawn an entity, draw a frame, name a color, run a scene. Less to choose, less to learn, less to maintain." + ], + "stats": [ + { "big": "0", "lbl": "engines to install" }, + { "big": "1", "lbl": "clear way to do each thing" }, + { "big": "=", "lbl": "deterministic frames" } + ] + }, + "start": { + "kicker": "Get started", + "title": "From clone to a native window.", + "intro": "Build the toolchain once, then your game. Everything runs from the repo root.", + "steps": [ + { "title": "Build the toolchain", "html": "bin/x build produces ludicc, the compiler you'll use for everything below." }, + { "title": "Compile & run an example", "html": "bin/x app examples/snake.ludic turns a .ludic file into a native binary. Run it to open a real window." }, + { "title": "Go headless for tests", "html": "--headless renders frames to a .ppm from piped input — deterministic output you can diff." }, + { "title": "Edit with full tooling", "html": "bin/x tools builds the formatter and language server; every editor gets completion, diagnostics and go-to-definition." } + ], + "terminal_name": "zsh — ludic", + "terminal": [ + { "comment": "build the toolchain (once)" }, + { "cmd": "bin/x build" }, + { "blank": true }, + { "comment": "build and run an example (opens a window)" }, + { "cmd": "bin/x app examples/snake.ludic" }, + { "cmd": "./build/snake" }, + { "blank": true }, + { "comment": "deterministic headless render for tests" }, + { "cmd": "bin/x app examples/snake.ludic --headless" }, + { "cmd": "printf 'ddddwww' | ./build/snake_headless" }, + { "out": "→ writes out.ppm" } + ] + }, + "editors": { + "kicker": "Editor experience", + "title": "One language server, every editor.", + "intro": "ludic-lsp speaks LSP 3.17 over stdio: context-aware completion, diagnostics from the compiler itself, go-to-definition and rename across imported files, and comment-preserving formatting. It even understands ```ludic fences in Markdown.", + "list": ["VS Code", "JetBrains IDEs", "Neovim", "Helix", "Emacs", "Sublime Text", "Zed"], + "note": "bin/x tools builds ludic-fmt and ludic-lsp — the same formatter runs as a CLI for pre-commit hooks and CI." + } +} diff --git a/docs/site/snippets/hero.ludic b/docs/site/snippets/hero.ludic new file mode 100644 index 00000000..fb147eb5 --- /dev/null +++ b/docs/site/snippets/hero.ludic @@ -0,0 +1,24 @@ +# the smallest program that exercises the whole ECS pipeline +program Hello { + + property Pos { x: int = 0, y: int = 0 } + property Vel { dx: int = 0, dy: int = 0 } + + handler Boot phase Start { + spawn Mob { Pos { x: 3, y: 4 } Vel { dx: 1, dy: 0 } } + spawn Mob { Pos { x: 10, y: 2 } Vel { dx: 0, dy: 1 } } + } + + # a handler declares the entities it touches; the body + # runs once per match, each property bound by name. + @Queries(these: [Pos, Vel]) + handler Move phase FixedUpdate { + Pos.x = Pos.x + Vel.dx + Pos.y = Pos.y + Vel.dy + } + + handler Report phase Update { + for (p) in query [Pos] { print(p.x) print(p.y) } + quit() + } +} diff --git a/docs/site/snippets/lifecycle.ludic b/docs/site/snippets/lifecycle.ludic new file mode 100644 index 00000000..6c4371d5 --- /dev/null +++ b/docs/site/snippets/lifecycle.ludic @@ -0,0 +1,20 @@ +program Toggles { + property Health { hp: int = 0, max: int = 100 } + property Shield { amount: int = 0 } + model Player { Health, Shield } + + # hooks fire at the toggle point, data bound by name + @OnDisable(Shield) handler Down { print(Shield.amount + 1) } + @OnEnable(Shield) handler Up { print(Shield.amount + 2) } + + handler Seed phase Start { + spawn Player { Health { max: 50 }, Shield { amount: 5 } } + } + + handler Run phase Render { + for (e) in query [Player] { disable Shield on self() } # @OnDisable + for (e) in query [Player] { enable Shield on self() } # @OnEnable, data intact + disable Player # whole model off + quit() + } +} diff --git a/docs/site/snippets/scenes.ludic b/docs/site/snippets/scenes.ludic new file mode 100644 index 00000000..4ebb7659 --- /dev/null +++ b/docs/site/snippets/scenes.ludic @@ -0,0 +1,30 @@ +program SceneDemo { + var counter: int = 0 + + handler Boot phase Start { counter = 0 print(1000) } + + scene Title start { + on enter { print(1) } + on exit { print(2) } + layer Main { + handler Tick phase Update { + counter = counter + 1 + print(100 + counter) + if counter >= 2 { become Play } # hand off to Play + } + } + } + + scene Play { + on enter { print(3) counter = 0 } + on exit { print(4) } + layer World { + handler Step phase Update { + counter = counter + 1 + print(200 + counter) + if counter >= 2 { quit() } + } + } + layer Hud { handler Draw phase Render { print(900) } } + } +} diff --git a/docs/site/snippets/snake.ludic b/docs/site/snippets/snake.ludic new file mode 100644 index 00000000..0223ae0e --- /dev/null +++ b/docs/site/snippets/snake.ludic @@ -0,0 +1,30 @@ +program Snake { + property Pos { x: int = 0, y: int = 0 } + property Seg { order: int = 0 } + + const GRID_W: int = 20 const GRID_H: int = 15 const TILE: int = 16 + + var food_x: int = 14 var food_y: int = 7 var score: int = 0 + + handler Draw phase Render { + Screen.clear(Color.MidnightBlue) + # checkerboard field — a fresh, immutable gx/gy each pass + for gy in 0 .. GRID_H { + for gx in 0 .. GRID_W { + var tile_color = Color.Charcoal + if (gx + gy) % 2 == 0 { tile_color = Color.Gunmetal } + Screen.fill_rectangle(x: gx * TILE, y: gy * TILE, width: TILE, height: TILE, color: tile_color) + } + } + # the food, then every snake segment straight from the ECS + Screen.fill_rectangle(x: food_x * TILE + 3, y: food_y * TILE + 3, width: TILE - 6, height: TILE - 6, color: Color.Crimson) + for (p, s) in query [Pos, Seg] { + var seg_color = Color.LimeGreen + if s.order == 0 { seg_color = Color.MintGreen } # brighter head + Screen.fill_rectangle(x: p.x * TILE + 1, y: p.y * TILE + 1, width: TILE - 2, height: TILE - 2, color: seg_color) + } + Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1) + Screen.draw_number(x: 52, y: 4, value: score, color: Color.Gold, scale: 1) + Screen.show() + } +} diff --git a/tools/docgen/README.md b/tools/docgen/README.md new file mode 100644 index 00000000..3ce8ce8c --- /dev/null +++ b/tools/docgen/README.md @@ -0,0 +1,67 @@ +# Ludic documentation generator + +Generates the public documentation site (the `pages` branch) from a single +source of truth, so the site can never drift from the language. + +## Source of truth + +``` +docs/ + language//.md one file per symbol — keyword, type, builtin, + namespace method, operator, annotation + language//_section.md section title + blurb + order + language/colors/palette.json the 221 named colors (generated by palette.py) + site/site.json landing-page messaging (hero, features, …) + site/snippets/*.ludic the code shown on the landing page (real programs) +``` + +### A symbol file + +```markdown +--- +id: kw-handler # anchor in api.html (kw-*, type-*, fn-*, -*, op-*, annot-*) +name: handler # display name / heading +category: control # which section (matches the directory) +kind: keyword # keyword | type | phase | namespace-method | builtin | annotation | operator +tokens: handler # the literal token(s) the highlighter recognizes & links +sig: handler Name phase P { … } +tip: A block that runs each frame in a given phase. # one-line hover tip +order: 3 +--- + +Full description — inline HTML (``, ``) and `backtick code` both work. + +```ludic +handler Move phase Update { … } +``` +``` + +Add such a file and it appears in the API Reference, is recognized + tipped + +linked in **every** code snippet across the site, and lands in `symbols.json` — +with no other file to edit. + +## Build + +```bash +python3 tools/docgen/gen.py --out build/pages # generate the whole site +python3 tools/docgen/check.py build/pages # sanity-check before publish +``` + +Outputs into `--out`: `index.html`, `api.html`, `ludic-highlight.js` +(its symbol tables generated from the sources above), `symbols.json`, `.nojekyll`. + +No third-party dependencies — Python standard library only. + +## Publish + +`.forgejo/workflows/docs.yml` runs this on every push to `main` that touches +`docs/**` or `tools/docgen/**`, and force-publishes the result to the `pages` +branch root (which the pages-server serves). It needs a repo secret +`PAGES_TOKEN` with write access (or the automatic Actions token enabled for +pushes). `index.html` + `.nojekyll` always stay at the branch root. + +## Colors + +`palette.json` is generated by `tools/docgen/palette.py` from a single `PALETTE` +table, which also generates `selfhost/emit_color.ludic`. Regenerate colors +there; never hand-edit either output. diff --git a/tools/docgen/assets/api.css b/tools/docgen/assets/api.css new file mode 100644 index 00000000..72f21c2d --- /dev/null +++ b/tools/docgen/assets/api.css @@ -0,0 +1,83 @@ + :root{ + --bg:#0a0d18; --bg2:#0d1122; --panel:#12172b; --panel2:#151b32; + --line:#232b47; --line2:#2c3660; --text:#c9d2ea; --head:#f2f5ff; --muted:#8290b4; + --mint:#7cf5c4; --coral:#ff5d73; --violet:#c792ea; --blue:#82aaff; --amber:#f6c177; + --c-bg:#0b0f20; --c-comment:#5b6788; --c-key:#ff7eb6; --c-type:#7cf5c4; + --c-str:#f6c177; --c-num:#a6b8ff; --c-annot:#c792ea; --c-fn:#82aaff; --c-punc:#9aa6cc; + --radius:14px; --max:1180px; + } + *{box-sizing:border-box} + html{scroll-behavior:smooth} + body{margin:0;background:var(--bg);color:var(--text); + font-family:"Space Grotesk",system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.6; + -webkit-font-smoothing:antialiased} + a{color:inherit;text-decoration:none} + code,pre,.mono{font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace} + .wrap{max-width:var(--max);margin:0 auto;padding:0 24px} + + header.nav{position:sticky;top:0;z-index:50;backdrop-filter:blur(10px); + background:rgba(10,13,24,.78);border-bottom:1px solid var(--line)} + .nav-in{display:flex;align-items:center;gap:22px;height:64px} + .brand{display:flex;align-items:center;gap:10px;font-weight:700;color:var(--head);font-size:18px} + .logo{width:26px;height:26px;border-radius:7px;display:grid;place-items:center; + background:linear-gradient(135deg,var(--mint),#3fd7a6);color:#06231a;font-weight:700;font-size:15px} + .nav-links{display:flex;gap:24px;margin-left:auto;align-items:center} + .nav-links a{color:var(--muted);font-size:14.5px;font-weight:500;transition:color .15s} + .nav-links a:hover{color:var(--head)} + .nav-cta{border:1px solid var(--line2);padding:8px 15px;border-radius:9px;color:var(--head)!important;background:var(--panel)} + .nav-cta:hover{border-color:var(--mint)} + @media(max-width:760px){.nav-links a:not(.nav-cta){display:none}} + + /* two-column reference layout */ + .ref-layout{display:grid;grid-template-columns:230px 1fr;gap:40px;align-items:start;padding-top:34px;padding-bottom:80px} + @media(max-width:900px){.ref-layout{grid-template-columns:1fr}.side{display:none}} + .side{position:sticky;top:88px;display:flex;flex-direction:column;gap:2px;max-height:calc(100vh - 110px);overflow:auto} + .side a{color:var(--muted);font-size:13.5px;padding:6px 10px;border-radius:8px;border-left:2px solid transparent;transition:all .12s} + .side a:hover{color:var(--head);background:var(--panel)} + .side a.active{color:var(--mint);border-left-color:var(--mint);background:rgba(124,245,196,.06)} + + .ref-intro{margin-bottom:44px} + .ref-intro .kicker{font-size:13px;font-weight:600;letter-spacing:2px;text-transform:uppercase;color:var(--mint);margin-bottom:12px} + .ref-intro h1{font-size:clamp(30px,5vw,46px);margin:0 0 12px;color:var(--head);letter-spacing:-1px;font-weight:700} + .ref-intro p{font-size:17px;color:var(--muted);max-width:640px;margin:0} + + .ref-sec{padding:26px 0 10px;border-top:1px solid var(--line);margin-top:26px} + .ref-sec:first-of-type{border-top:none;margin-top:0} + .ref-sec h2{font-size:24px;color:var(--head);margin:0 0 6px;letter-spacing:-.4px} + .sec-blurb{color:var(--muted);font-size:15px;margin:0 0 22px;max-width:720px} + .sec-blurb code{color:var(--mint);background:rgba(124,245,196,.08);padding:1px 5px;border-radius:5px;font-size:12.5px} + + .entry{padding:16px 0;border-top:1px dashed var(--line)} + .entry:first-of-type{border-top:none} + .entry-head{display:flex;align-items:center;gap:10px} + .entry h3{margin:0;font-size:16.5px;color:var(--head);font-weight:600;font-family:"JetBrains Mono",monospace} + .entry .anchor{color:var(--line2);font-size:15px;opacity:0;transition:opacity .12s} + .entry:hover .anchor{opacity:1} + .entry .anchor:hover{color:var(--mint)} + .entry .sig{display:inline-block;margin:8px 0 6px;color:var(--c-fn);background:var(--c-bg); + border:1px solid var(--line);border-radius:8px;padding:5px 10px;font-size:13px} + .entry p{margin:6px 0 0;color:var(--text);font-size:14.5px;max-width:720px} + .entry p code{color:var(--amber);background:var(--panel);padding:1px 5px;border-radius:5px;font-size:12.5px} + .entry p b{color:var(--head)} + + pre{margin:10px 0 0;padding:14px 16px;overflow:auto;font-size:13px;line-height:1.7;tab-size:2; + background:var(--c-bg);border:1px solid var(--line);border-radius:10px} + pre.ex{max-width:720px} + pre::-webkit-scrollbar{height:8px} + pre::-webkit-scrollbar-thumb{background:var(--line2);border-radius:6px} + .t-com{color:var(--c-comment);font-style:italic} + .t-key{color:var(--c-key)}.t-type{color:var(--c-type)}.t-str{color:var(--c-str)} + .t-num{color:var(--c-num)}.t-annot{color:var(--c-annot)}.t-fn{color:var(--c-fn)}.t-punc{color:var(--c-punc)} + /* hover-jump links inside code */ + a.tok{border-radius:3px;transition:background .12s} + a.tok:hover{background:rgba(124,245,196,.14);outline:1px solid rgba(124,245,196,.35)} + + /* color swatches */ + .swatch-group{margin-bottom:22px} + .swatch-group h4{margin:0 0 12px;font-size:13px;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted);font-weight:600} + .swatch-row{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px} + .swatch{display:flex;align-items:center;gap:10px;background:var(--panel);border:1px solid var(--line); + border-radius:9px;padding:8px 10px} + .chip{width:24px;height:24px;border-radius:6px;flex:none;box-shadow:inset 0 0 0 1px rgba(255,255,255,.12)} + .cname{font-family:"JetBrains Mono",monospace;font-size:12px;color:var(--head);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + .chex{margin-left:auto;font-family:"JetBrains Mono",monospace;font-size:11px;color:var(--muted)} diff --git a/tools/docgen/assets/ludic-highlight.tmpl.js b/tools/docgen/assets/ludic-highlight.tmpl.js new file mode 100644 index 00000000..3185e044 --- /dev/null +++ b/tools/docgen/assets/ludic-highlight.tmpl.js @@ -0,0 +1,132 @@ +/* ============================================================================ + * ludic-highlight.js — the Ludic syntax highlighter for the docs site. + * + * GENERATED FILE — do not edit by hand. The symbol tables below (keywords, + * types, phases, namespace methods, builtins, annotations and their one-line + * tips + anchors) are produced by tools/docgen/gen.py from the per-symbol + * source files in docs/language/**. Add a symbol there and it is recognized, + * colored, tipped and linked here automatically — nothing to maintain twice. + * + * Beyond coloring, every token the language defines becomes a link into the API + * Reference (api.html): hover a keyword, a Screen.* call, a named color, a + * builtin or a type and it points at the entry that explains it. Your own + * symbols (functions, entities, fields) stay plain. + * ========================================================================== */ +(function (global) { + "use strict"; + + const SYMBOLS = /*__SYMBOLS__*/{}; + const KEYWORDS = SYMBOLS.keywords || {}; + const TYPES = SYMBOLS.types || {}; + const PHASES = SYMBOLS.phases || {}; + const BUILTINS = SYMBOLS.builtins || {}; + const NSMETHODS = SYMBOLS.nsmethods || {}; + const ANNOTS = SYMBOLS.annotations || {}; + const TIPS = SYMBOLS.tips || {}; + const NAMESPACES = new Set(SYMBOLS.namespaces || []); + const COLORS_ANCHOR = SYMBOLS.colors_anchor || "colors"; + const ANNOT_ANCHOR = SYMBOLS.annotations_anchor || "annotations"; + + function esc(s) { + return s.replace(/&/g, "&").replace(//g, ">"); + } + const isIdStart = c => /[A-Za-z_]/.test(c); + const isId = c => /[A-Za-z0-9_]/.test(c); + + function link(href, tip, inner) { + const t = tip ? ' title="' + esc(tip) + '"' : ""; + return '" + inner + ""; + } + + function highlight(src) { + let out = "", i = 0; + const n = src.length; + while (i < n) { + const c = src[i]; + // comment + if (c === "#") { + let j = i; while (j < n && src[j] !== "\n") j++; + out += '' + esc(src.slice(i, j)) + ""; + i = j; continue; + } + // string / char / interpolation (backtick) + if (c === '"' || c === "'" || c === "`") { + const q = c; let j = i + 1; + while (j < n && src[j] !== q) { if (src[j] === "\\") j++; j++; } + j = Math.min(j + 1, n); + out += '' + esc(src.slice(i, j)) + ""; + i = j; continue; + } + // annotation @Name + if (c === "@") { + let j = i + 1; while (j < n && isId(src[j])) j++; + const at = src.slice(i, j); + const anchor = ANNOTS[at] || ANNOT_ANCHOR; + const tip = TIPS[at] || "A compile-time annotation."; + out += link("api.html#" + anchor, tip, + '' + esc(at) + ""); + i = j; continue; + } + // number (incl 0x hex) + if (/[0-9]/.test(c)) { + let j = i; while (j < n && /[0-9a-fA-FxX._]/.test(src[j])) j++; + out += '' + esc(src.slice(i, j)) + ""; + i = j; continue; + } + // identifier / keyword / type / namespace.member / builtin + if (isIdStart(c)) { + let j = i; while (j < n && isId(src[j])) j++; + const word = src.slice(i, j); + + // Namespace.member (Screen.fill_rectangle, Color.Crimson, …) + if (NAMESPACES.has(word) && src[j] === "." && j + 1 < n && isIdStart(src[j + 1])) { + let k = j + 1; while (k < n && isId(src[k])) k++; + const member = src.slice(j + 1, k); + const key = word + "." + member; + let href, tip; + if (word === "Color") { + href = "api.html#" + COLORS_ANCHOR; tip = "Named color " + key + "."; + } else { + href = "api.html#" + (NSMETHODS[key] || (word.toLowerCase() + "-" + member)); + tip = TIPS[key] || key; + } + const inner = '' + word + '.' + esc(member) + ""; + out += link(href, tip, inner); + i = k; continue; + } + + let k2 = j; while (k2 < n && src[k2] === " ") k2++; + const callish = src[k2] === "("; + if (KEYWORDS[word]) { + out += link("api.html#" + KEYWORDS[word], TIPS[word], '' + word + ""); + } else if (BUILTINS[word] && callish) { + out += link("api.html#" + BUILTINS[word], TIPS[word], '' + word + ""); + } else if (TYPES[word]) { + out += link("api.html#" + TYPES[word], TIPS[word] || ("The " + word + " type."), '' + word + ""); + } else if (PHASES[word]) { + out += link("api.html#" + PHASES[word], "The " + word + " phase.", '' + word + ""); + } else if (callish) { + out += '' + esc(word) + ""; + } else { + out += esc(word); + } + i = j; continue; + } + // punctuation + if (/[{}\[\]()=<>+\-*\/%,.:;!&|~]/.test(c)) { + out += '' + esc(c) + ""; + i++; continue; + } + out += esc(c); i++; + } + return out; + } + + function highlightAll() { + document.querySelectorAll('pre[data-lang="ludic"]').forEach(pre => { + pre.innerHTML = highlight(pre.textContent); + }); + } + + global.Ludic = { highlight, highlightAll, SYMBOLS }; +})(window); diff --git a/tools/docgen/assets/site.css b/tools/docgen/assets/site.css new file mode 100644 index 00000000..8b7b10cb --- /dev/null +++ b/tools/docgen/assets/site.css @@ -0,0 +1,194 @@ + :root{ + --bg:#0a0d18; + --bg2:#0d1122; + --panel:#12172b; + --panel2:#151b32; + --line:#232b47; + --line2:#2c3660; + --text:#c9d2ea; + --head:#f2f5ff; + --muted:#8290b4; + --mint:#7cf5c4; + --coral:#ff5d73; + --violet:#c792ea; + --blue:#82aaff; + --amber:#f6c177; + --peri:#a6b8ff; + /* code theme */ + --c-bg:#0b0f20; + --c-comment:#5b6788; + --c-key:#ff7eb6; + --c-type:#7cf5c4; + --c-str:#f6c177; + --c-num:#a6b8ff; + --c-annot:#c792ea; + --c-fn:#82aaff; + --c-punc:#9aa6cc; + --radius:14px; + --max:1120px; + } + *{box-sizing:border-box} + html{scroll-behavior:smooth} + body{ + margin:0; + background: + radial-gradient(1100px 600px at 82% -8%, rgba(124,245,196,.10), transparent 60%), + radial-gradient(900px 620px at 6% 4%, rgba(255,93,115,.09), transparent 55%), + var(--bg); + color:var(--text); + font-family:"Space Grotesk", system-ui, -apple-system, Segoe UI, Roboto, sans-serif; + line-height:1.6; + -webkit-font-smoothing:antialiased; + text-rendering:optimizeLegibility; + } + a{color:inherit;text-decoration:none} + code,pre,.mono{font-family:"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace} + .wrap{max-width:var(--max);margin:0 auto;padding:0 24px} + + /* ---------- nav ---------- */ + header.nav{position:sticky;top:0;z-index:50;backdrop-filter:blur(10px); + background:rgba(10,13,24,.72);border-bottom:1px solid var(--line)} + .nav-in{display:flex;align-items:center;gap:22px;height:64px} + .brand{display:flex;align-items:center;gap:10px;font-weight:700;color:var(--head);font-size:18px;letter-spacing:.2px} + .logo{width:26px;height:26px;border-radius:7px;display:grid;place-items:center; + background:linear-gradient(135deg,var(--mint),#3fd7a6);color:#06231a;font-weight:700;font-size:15px; + box-shadow:0 0 0 1px rgba(124,245,196,.3), 0 6px 20px -6px rgba(124,245,196,.5)} + .nav-links{display:flex;gap:24px;margin-left:auto;align-items:center} + .nav-links a{color:var(--muted);font-size:14.5px;font-weight:500;transition:color .15s} + .nav-links a:hover{color:var(--head)} + .nav-cta{border:1px solid var(--line2);padding:8px 15px;border-radius:9px;color:var(--head)!important; + background:var(--panel);transition:border-color .15s, background .15s} + .nav-cta:hover{border-color:var(--mint);background:var(--panel2)} + @media(max-width:760px){.nav-links a:not(.nav-cta){display:none}} + + /* ---------- hero ---------- */ + .hero{padding:84px 0 56px;position:relative;overflow:hidden} + .pill{display:inline-flex;align-items:center;gap:9px;font-size:13px;color:var(--mint); + border:1px solid rgba(124,245,196,.28);background:rgba(124,245,196,.06); + padding:6px 13px;border-radius:999px;font-weight:500;margin-bottom:26px} + .pill .dot{width:7px;height:7px;border-radius:50%;background:var(--mint);box-shadow:0 0 8px var(--mint)} + h1{font-size:clamp(38px,6vw,68px);line-height:1.04;margin:0 0 20px;color:var(--head); + font-weight:700;letter-spacing:-1.5px} + h1 .accent{background:linear-gradient(120deg,var(--mint),var(--blue));-webkit-background-clip:text; + background-clip:text;-webkit-text-fill-color:transparent} + .lead{font-size:clamp(17px,2.3vw,21px);color:var(--muted);max-width:640px;margin:0 0 34px} + .lead b{color:var(--text);font-weight:600} + .cta-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center} + .btn{display:inline-flex;align-items:center;gap:9px;font-weight:600;font-size:15.5px; + padding:13px 22px;border-radius:11px;transition:transform .12s, box-shadow .15s, border-color .15s;font-family:inherit;cursor:pointer;border:1px solid transparent} + .btn:active{transform:translateY(1px)} + .btn-primary{background:linear-gradient(135deg,var(--mint),#43dcae);color:#06231a; + box-shadow:0 10px 30px -10px rgba(124,245,196,.6)} + .btn-primary:hover{box-shadow:0 14px 38px -10px rgba(124,245,196,.75)} + .btn-ghost{border:1px solid var(--line2);color:var(--head);background:var(--panel)} + .btn-ghost:hover{border-color:var(--mint)} + .hero-grid{display:grid;grid-template-columns:1.02fr .98fr;gap:48px;align-items:center} + @media(max-width:900px){.hero-grid{grid-template-columns:1fr;gap:36px}} + + /* window chrome for code */ + .code-card{background:var(--c-bg);border:1px solid var(--line);border-radius:var(--radius); + overflow:hidden;box-shadow:0 30px 60px -30px rgba(0,0,0,.7)} + .code-top{display:flex;align-items:center;gap:8px;padding:12px 15px;border-bottom:1px solid var(--line); + background:linear-gradient(180deg,var(--panel),rgba(18,23,43,.4))} + .tl{width:11px;height:11px;border-radius:50%} + .tl.r{background:#ff5f57}.tl.y{background:#febc2e}.tl.g{background:#28c840} + .code-name{margin-left:8px;font-size:12.5px;color:var(--muted)} + pre{margin:0;padding:20px 22px;overflow:auto;font-size:13.5px;line-height:1.72;tab-size:2} + pre::-webkit-scrollbar{height:9px;width:9px} + pre::-webkit-scrollbar-thumb{background:var(--line2);border-radius:6px} + /* token colors */ + .t-com{color:var(--c-comment);font-style:italic} + .t-key{color:var(--c-key)} + .t-type{color:var(--c-type)} + .t-str{color:var(--c-str)} + .t-num{color:var(--c-num)} + .t-annot{color:var(--c-annot)} + .t-fn{color:var(--c-fn)} + .t-punc{color:var(--c-punc)} + /* hover-to-jump links inside code snippets */ + pre a.tok{border-radius:3px;transition:background .12s, outline-color .12s} + pre a.tok:hover{background:rgba(124,245,196,.14);outline:1px solid rgba(124,245,196,.35)} + + /* ---------- pipeline ---------- */ + .pipeline{display:flex;align-items:center;justify-content:center;gap:0;flex-wrap:wrap; + margin:8px 0 0;padding:20px;border:1px dashed var(--line2);border-radius:var(--radius); + background:rgba(18,23,43,.4)} + .stage{font-family:"JetBrains Mono",monospace;font-size:13px;font-weight:500;color:var(--head); + background:var(--panel);border:1px solid var(--line2);padding:8px 13px;border-radius:9px;white-space:nowrap} + .stage.hl{color:var(--mint);border-color:rgba(124,245,196,.35)} + .arrow{color:var(--muted);padding:0 12px;font-size:15px} + + /* ---------- sections ---------- */ + section{padding:72px 0} + .sec-head{max-width:680px;margin-bottom:44px} + .kicker{font-size:13px;font-weight:600;letter-spacing:2px;text-transform:uppercase;color:var(--mint);margin-bottom:14px} + h2{font-size:clamp(28px,4vw,40px);margin:0 0 14px;color:var(--head);letter-spacing:-.8px;font-weight:700;line-height:1.1} + .sec-head p{font-size:17.5px;color:var(--muted);margin:0} + + /* feature grid */ + .feat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px} + @media(max-width:900px){.feat-grid{grid-template-columns:repeat(2,1fr)}} + @media(max-width:600px){.feat-grid{grid-template-columns:1fr}} + .feat{background:linear-gradient(180deg,var(--panel),var(--bg2));border:1px solid var(--line); + border-radius:var(--radius);padding:24px 22px;transition:transform .16s, border-color .16s} + .feat:hover{transform:translateY(-3px);border-color:var(--line2)} + .feat .ico{width:40px;height:40px;border-radius:10px;display:grid;place-items:center;margin-bottom:16px; + background:rgba(124,245,196,.09);border:1px solid rgba(124,245,196,.2);font-size:20px} + .feat h3{margin:0 0 8px;font-size:17.5px;color:var(--head);font-weight:600} + .feat p{margin:0;font-size:14.5px;color:var(--muted);line-height:1.6} + .feat p code{color:var(--mint);font-size:12.5px;background:rgba(124,245,196,.08);padding:1px 5px;border-radius:5px} + + /* showcase tabs */ + .tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:18px} + .tab{font-family:"JetBrains Mono",monospace;font-size:13px;color:var(--muted);background:var(--panel); + border:1px solid var(--line);padding:8px 15px;border-radius:9px;cursor:pointer;transition:all .14s;font-weight:500} + .tab:hover{color:var(--text);border-color:var(--line2)} + .tab.active{color:#06231a;background:var(--mint);border-color:var(--mint);font-weight:600} + .panel-code{display:none} + .panel-code.active{display:block} + .showcase-note{margin-top:16px;font-size:14px;color:var(--muted);display:flex;gap:10px;align-items:flex-start} + .showcase-note .b{color:var(--mint);font-weight:700} + + /* steps */ + .steps{display:grid;grid-template-columns:1.1fr 1fr;gap:40px;align-items:start} + @media(max-width:900px){.steps{grid-template-columns:1fr}} + .step{display:flex;gap:16px;margin-bottom:26px} + .step .n{flex:none;width:32px;height:32px;border-radius:9px;display:grid;place-items:center;font-weight:700; + font-family:"JetBrains Mono",monospace;font-size:14px;color:var(--mint); + background:rgba(124,245,196,.08);border:1px solid rgba(124,245,196,.25)} + .step h4{margin:2px 0 6px;color:var(--head);font-size:16.5px;font-weight:600} + .step p{margin:0;color:var(--muted);font-size:14.5px} + .step p code{color:var(--text);background:var(--panel);padding:1px 6px;border-radius:5px;font-size:12.5px;border:1px solid var(--line)} + + .term{background:var(--c-bg);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;position:sticky;top:88px} + .term pre{font-size:13px} + .term .prompt{color:var(--mint)} + .term .out{color:var(--muted)} + + /* editors strip */ + .editors{display:flex;flex-wrap:wrap;gap:12px} + .ed{font-family:"JetBrains Mono",monospace;font-size:13.5px;color:var(--text);background:var(--panel); + border:1px solid var(--line);border-radius:10px;padding:11px 16px;display:flex;align-items:center;gap:9px} + .ed .k{color:var(--mint)} + + /* callout / philosophy banner */ + .banner{background:linear-gradient(120deg,rgba(124,245,196,.08),rgba(130,170,255,.06)); + border:1px solid var(--line2);border-radius:18px;padding:38px 34px;display:grid;grid-template-columns:1.2fr 1fr;gap:34px;align-items:center} + @media(max-width:860px){.banner{grid-template-columns:1fr}} + .banner h2{font-size:clamp(24px,3.4vw,32px)} + .banner p{color:var(--muted);margin:0 0 6px;font-size:15.5px} + .stat-row{display:flex;gap:14px;flex-wrap:wrap} + .stat{background:var(--c-bg);border:1px solid var(--line);border-radius:12px;padding:16px 18px;flex:1;min-width:130px} + .stat .big{font-family:"JetBrains Mono",monospace;font-size:26px;color:var(--mint);font-weight:700;line-height:1} + .stat .lbl{font-size:12.5px;color:var(--muted);margin-top:8px} + + /* footer */ + footer{border-top:1px solid var(--line);padding:44px 0 60px;margin-top:20px} + .foot-in{display:flex;justify-content:space-between;gap:24px;flex-wrap:wrap;align-items:center} + .foot-in .muted{color:var(--muted);font-size:14px} + .foot-links{display:flex;gap:22px;flex-wrap:wrap} + .foot-links a{color:var(--muted);font-size:14px;transition:color .15s} + .foot-links a:hover{color:var(--mint)} + + .reveal{opacity:0;transform:translateY(16px);transition:opacity .6s ease, transform .6s ease} + .reveal.in{opacity:1;transform:none} diff --git a/tools/docgen/check.py b/tools/docgen/check.py new file mode 100644 index 00000000..57a65ce6 --- /dev/null +++ b/tools/docgen/check.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""check.py — sanity-check a generated docs site before it is published. + +Verifies: + * the pages-server contract: index.html + .nojekyll exist at the root; + * api.html and ludic-highlight.js are present; + * every anchor the highlighter links to actually exists in api.html + (so no code-snippet token points at a missing entry). + +Usage: python3 tools/docgen/check.py +Exit code 1 on any failure. +""" +import json, os, re, sys + +def main(site): + problems = [] + need = ["index.html", "api.html", "ludic-highlight.js", ".nojekyll", "symbols.json"] + for f in need: + if not os.path.exists(os.path.join(site, f)): + problems.append(f"missing required file: {f}") + if os.path.exists(os.path.join(site, "symbols.json")) and os.path.exists(os.path.join(site, "api.html")): + sym = json.load(open(os.path.join(site, "symbols.json"))) + ids = set(re.findall(r'id="([^"]+)"', open(os.path.join(site, "api.html")).read())) + anchors = set() + for grp in ("keywords", "types", "phases", "builtins", "nsmethods", "annotations"): + anchors |= set(sym.get(grp, {}).values()) + anchors.add(sym.get("colors_anchor", "colors")) + anchors.add(sym.get("annotations_anchor", "annotations")) + for a in sorted(anchors): + if a not in ids: + problems.append(f"highlighter links to #{a} but api.html has no such anchor") + if problems: + print("docs check FAILED:") + for p in problems: + print(" -", p) + return 1 + print("docs check OK:", site) + return 0 + +if __name__ == "__main__": + sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "build/pages")) diff --git a/tools/docgen/gen.py b/tools/docgen/gen.py new file mode 100644 index 00000000..b86996b2 --- /dev/null +++ b/tools/docgen/gen.py @@ -0,0 +1,525 @@ +#!/usr/bin/env python3 +"""gen.py — the Ludic documentation generator. + +Single source of truth: + docs/language//.md one file per keyword / type / builtin / + namespace method / operator / annotation + docs/language//_section.md section title + blurb + order + docs/language/colors/palette.json the named-color palette (from palette.py) + docs/site/site.json landing-page messaging (hero, features, …) + docs/site/snippets/*.ludic the code snippets shown on the landing page + +Outputs (into --out, default build/pages) — the whole pages-branch payload: + api.html the full API Reference, one entry per symbol + index.html the landing page (hero + showcase from real .ludic files) + ludic-highlight.js the highlighter, its symbol tables generated from the above + symbols.json the machine-readable symbol index (also useful to editors) + .nojekyll + +Nothing here is hand-maintained twice: add a symbol file and it appears in the +reference, is recognized + tipped + linked in every snippet, and lands in +symbols.json — automatically. Zero third-party dependencies (stdlib only). +""" +import json, os, html, re, sys, argparse + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets") +LANG = os.path.join(ROOT, "docs", "language") +SITE = os.path.join(ROOT, "docs", "site") + +def esc(s): return html.escape(s, quote=False) + +def fill(tmpl, mapping): + """Placeholder substitution that never collides with % or { } in CSS/JS.""" + for k, v in mapping.items(): + tmpl = tmpl.replace("@@" + k + "@@", str(v)) + return tmpl + +def pct(tmpl, mapping): + """Substitute only %(name)s markers; leave bare % (CSS 100%, code % 2) alone. + Replacement values are inserted literally and never re-scanned.""" + return re.sub(r"%\((\w+)\)s", lambda m: mapping[m.group(1)], tmpl) + +# --------------------------------------------------------------------------- +# front-matter + body parsing (no yaml dependency) +# --------------------------------------------------------------------------- +def parse_doc(path): + text = open(path, encoding="utf-8").read() + meta, body = {}, text + if text.startswith("---"): + end = text.find("\n---", 3) + if end != -1: + fm = text[3:end].strip("\n") + body = text[end + 4:].lstrip("\n") + for line in fm.split("\n"): + if not line.strip() or ":" not in line: + continue + k, v = line.split(":", 1) + meta[k.strip()] = v.strip() + return meta, body + +FENCE = re.compile(r"```ludic\n(.*?)\n```", re.S) + +def split_body(body): + """Return (description_html, [examples]) — fences pulled out as examples.""" + examples = FENCE.findall(body) + desc = FENCE.sub("", body).strip() + # author convenience: `code` -> code (leaves existing tags alone) + desc = re.sub(r"`([^`]+)`", r"\1", desc) + paras = [p.strip() for p in re.split(r"\n\s*\n", desc) if p.strip()] + desc_html = "

".join(paras) + return desc_html, examples + +# --------------------------------------------------------------------------- +# load the symbol model +# --------------------------------------------------------------------------- +def load_sections(): + sections = [] + for cat in sorted(os.listdir(LANG)): + cdir = os.path.join(LANG, cat) + if not os.path.isdir(cdir): + continue + smeta, sblurb = {}, "" + secpath = os.path.join(cdir, "_section.md") + if os.path.exists(secpath): + smeta, sbody = parse_doc(secpath) + sblurb = re.sub(r"`([^`]+)`", r"\1", sbody.strip()) + entries = [] + for fn in os.listdir(cdir): + if not fn.endswith(".md") or fn == "_section.md": + continue + meta, body = parse_doc(os.path.join(cdir, fn)) + desc, examples = split_body(body) + meta["desc_html"] = desc + meta["examples"] = examples + meta["tokens_list"] = meta.get("tokens", "").split() if meta.get("tokens") else [] + entries.append(meta) + entries.sort(key=lambda e: (int(e.get("order", 999)), e.get("name", ""))) + sections.append({ + "id": smeta.get("id", cat), + "title": smeta.get("title", cat.title()), + "order": int(smeta.get("order", 999)), + "blurb": sblurb, + "entries": entries, + }) + sections.sort(key=lambda s: (s["order"], s["title"])) + return sections + +# --------------------------------------------------------------------------- +# build the highlighter symbol tables from the model +# --------------------------------------------------------------------------- +def build_symbols(sections): + sym = {"keywords": {}, "types": {}, "phases": {}, "builtins": {}, + "nsmethods": {}, "annotations": {}, "tips": {}, + "namespaces": [], "colors_anchor": "colors", "annotations_anchor": "annotations"} + namespaces = set() + for s in sections: + for e in s["entries"]: + kind = e.get("kind", "") + anchor = e["id"] + tip = e.get("tip", "") + for tok in e["tokens_list"]: + if kind == "keyword": + sym["keywords"][tok] = anchor + elif kind == "type": + sym["types"][tok] = anchor + elif kind == "phase": + sym["phases"][tok] = anchor + elif kind == "builtin": + sym["builtins"][tok] = anchor + elif kind == "namespace-method": + sym["nsmethods"][tok] = anchor + if "." in tok: + namespaces.add(tok.split(".", 1)[0]) + elif kind == "annotation": + sym["annotations"][tok] = anchor + if tip: + sym["tips"][tok] = tip + if s["id"] == "colors": + sym["colors_anchor"] = "colors" + if s["id"] == "annotations": + sym["annotations_anchor"] = "annotations" + namespaces.add("Color") # Color.* is recognized and linked to the palette + sym["namespaces"] = sorted(namespaces) + return sym + +# --------------------------------------------------------------------------- +# render the API Reference +# --------------------------------------------------------------------------- +def render_entry(e): + ex = "" + for code in e.get("examples", []): + ex += '

' + esc(code) + "
" + desc = e.get("desc_html", "") + return ( + '
' + '

{name}

#
' + '{sig}' + '

{desc}

{ex}
' + ).format(id=e["id"], name=esc(e.get("name", "")), sig=esc(e.get("sig", "")), + desc=desc, ex=ex) + +def render_palette(palette): + out = ['
'] + for grp in palette["groups"]: + out.append('

' + esc(grp["name"]) + '

') + for col in grp["colors"]: + h = col["hex"] + out.append( + '
' + 'Color.{n}#{h}
' + .format(h=h, n=esc(col["name"]))) + out.append("
") + out.append("
") + return "\n".join(out) + +def render_section(s, palette): + if s["id"] == "colors": + body = render_palette(palette) + else: + body = "\n".join(render_entry(e) for e in s["entries"]) + return ('

{title}

' + '

{blurb}

{body}
').format( + id=s["id"], title=esc(s["title"]), blurb=s["blurb"], body=body) + +def render_api(sections, palette, cfg): + css = open(os.path.join(ASSETS, "api.css")).read() + nav = '" + content = "\n".join(render_section(s, palette) for s in sections) + navlinks = "".join( + '{l}'.format( + h=n["href"], l=esc(n["label"]), + cls='class="nav-cta" ' if n.get("href") == "api.html" else "") + for n in cfg["nav_links"]) + tmpl = """ + + + + +Ludic — API Reference + + + + + + + + +
+ @@NAV@@ +
+
+
Reference
+

API Reference

+

Every keyword, type, builtin, namespace and color in Ludic. In any code sample across this site, hover a token and click to jump straight to its entry here.

+
+ @@CONTENT@@ +
+
+ + + + +""" + return fill(tmpl, dict(CSS=css, BRAND=esc(cfg["brand"]), NAVLINKS=navlinks, NAV=nav, CONTENT=content)) + +# --------------------------------------------------------------------------- +# render the landing page +# --------------------------------------------------------------------------- +def read_snippet(rel): + return open(os.path.join(ROOT, rel), encoding="utf-8").read().rstrip("\n") + +def render_index(cfg): + css = open(os.path.join(ASSETS, "site.css")).read() + hero = cfg["hero"] + navlinks = "".join( + '{l}'.format( + h=n["href"], l=esc(n["label"]), + cls='class="nav-cta" ' if n.get("href") == "api.html" else "") + for n in cfg["nav_links"]) + # hero snippet + hero_code = esc(read_snippet(hero["snippet"])) + # pipeline + stages = "" + for st in hero["pipeline"]: + hl = " hl" if st == "ludicc" else "" + stages += '%s' % (hl, esc(st)) + if st != hero["pipeline"][-1]: + stages += '→' + # features + feats = "" + for c in cfg["features"]["cards"]: + feats += ('
%s
' + '

%s

%s

') % (c["icon"], c["title"], c["html"]) + # philosophy + phil = cfg["philosophy"] + phil_paras = "".join("

%s

" % p for p in phil["paras"]) + phil_stats = "".join('
%s
%s
' + % (s["big"], esc(s["lbl"])) for s in phil["stats"]) + # get-started steps + terminal + start = cfg["start"] + steps = "" + for i, s in enumerate(start["steps"], 1): + steps += ('
%d
' + '

%s

%s

') % (i, s["title"], s["html"]) + term = "" + for t in start["terminal"]: + if t.get("blank"): + term += "\n" + elif "comment" in t: + term += '# %s\n' % esc(t["comment"]) + elif "cmd" in t: + term += '$ %s\n' % esc(t["cmd"]) + elif "out" in t: + term += '%s\n' % esc(t["out"]) + # editors + ed = cfg["editors"] + eds = "".join('
◆ %s
' % esc(x) for x in ed["list"]) + # showcase samples -> JS array, code read from real files + samples = [] + for s in cfg["showcase"]["samples"]: + samples.append({"name": s["name"], "label": s["label"], + "note": s["note"], "code": read_snippet(s["file"])}) + samples_json = json.dumps(samples) + # footer links + footlinks = "".join('%s' % (n["href"], esc(n["label"])) for n in cfg["nav_links"]) + footlinks += 'Source ↗' % cfg["repo_url"] + m = cfg["meta"] + + tmpl = """ + + + + +%(title)s + + + + + + + + + + + + + +
+
+
+ %(pill)s +

%(title_pre)s %(title_accent)s

+

%(lead)s

+ +
+
+
+ + %(hero_name)s +
+
%(hero_code)s
+
+
+
+
%(stages)s
+

%(pipenote)s

+
+
+ +
+
+
+
%(feat_kicker)s
+

%(feat_title)s

+

%(feat_intro)s

+
+
%(feats)s
+
+
+ +
+
+
+
%(sc_kicker)s
+

%(sc_title)s

+

%(sc_intro)s

+
+
+
+
↳
+
+
+ +
+
+ +
+
+ +
+
+
+
%(start_kicker)s
+

%(start_title)s

+

%(start_intro)s

+
+
+
%(steps)s
+
+
+ + %(term_name)s +
+
%(term)s
+
+
+
+
+ +
+
+
+
%(ed_kicker)s
+

%(ed_title)s

+

%(ed_intro)s

+
+
%(eds)s
+

%(ed_note)s

+
+
+ +
+
+
+
%(brand)s
+
%(tagline)s
+
+ +
+
+ + + + + +""" + return pct(tmpl, dict( + css=css, brand=esc(cfg["brand"]), tagline=esc(cfg["tagline"]), repo=cfg["repo_url"], + title=esc(m["title"]), desc=esc(m["description"]), ogt=esc(m["og_title"]), ogd=esc(m["og_description"]), + navlinks=navlinks, + pill=esc(hero["pill"]), title_pre=esc(hero["title_pre"]), title_accent=esc(hero["title_accent"]), + lead=hero["lead"], pcta_h=hero["primary_cta"]["href"], pcta_l=esc(hero["primary_cta"]["label"]), + scta_h=hero["secondary_cta"]["href"], scta_l=esc(hero["secondary_cta"]["label"]), + hero_name=esc(hero["snippet_name"]), hero_code=hero_code, stages=stages, pipenote=hero["pipeline_note"], + feat_kicker=esc(cfg["features"]["kicker"]), feat_title=esc(cfg["features"]["title"]), + feat_intro=cfg["features"]["intro"], feats=feats, + sc_kicker=esc(cfg["showcase"]["kicker"]), sc_title=esc(cfg["showcase"]["title"]), + sc_intro=cfg["showcase"]["intro"], + phil_kicker=esc(phil["kicker"]), phil_title=esc(phil["title"]), phil_paras=phil_paras, phil_stats=phil_stats, + start_kicker=esc(start["kicker"]), start_title=esc(start["title"]), start_intro=esc(start["intro"]), + steps=steps, term_name=esc(start["terminal_name"]), term=term, + ed_kicker=esc(cfg["editors"]["kicker"]), ed_title=esc(cfg["editors"]["title"]), + ed_intro=cfg["editors"]["intro"], eds=eds, ed_note=cfg["editors"]["note"], + footlinks=footlinks, samples=samples_json, + )) + +# --------------------------------------------------------------------------- +def render_highlighter(symbols): + tmpl = open(os.path.join(ASSETS, "ludic-highlight.tmpl.js")).read() + return tmpl.replace("/*__SYMBOLS__*/{}", json.dumps(symbols, ensure_ascii=False)) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--out", default=os.path.join(ROOT, "build", "pages")) + args = ap.parse_args() + out = args.out + os.makedirs(out, exist_ok=True) + + sections = load_sections() + palette = json.load(open(os.path.join(LANG, "colors", "palette.json"))) + symbols = build_symbols(sections) + cfg = json.load(open(os.path.join(SITE, "site.json"))) + + open(os.path.join(out, "api.html"), "w").write(render_api(sections, palette, cfg)) + open(os.path.join(out, "index.html"), "w").write(render_index(cfg)) + open(os.path.join(out, "ludic-highlight.js"), "w").write(render_highlighter(symbols)) + open(os.path.join(out, "symbols.json"), "w").write(json.dumps(symbols, indent=2, ensure_ascii=False)) + open(os.path.join(out, ".nojekyll"), "w").write("") + + n_entries = sum(len(s["entries"]) for s in sections) + print("docs generated -> %s" % out) + print(" sections: %d symbols: %d colors: %d" + % (len(sections), n_entries, palette["count"])) + print(" highlighter tokens: %d kw / %d type / %d builtin / %d ns-method / %d annot" + % (len(symbols["keywords"]), len(symbols["types"]), len(symbols["builtins"]), + len(symbols["nsmethods"]), len(symbols["annotations"]))) + +if __name__ == "__main__": + main() diff --git a/tools/docgen/palette.py b/tools/docgen/palette.py new file mode 100644 index 00000000..be10622a --- /dev/null +++ b/tools/docgen/palette.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 +# Single source of truth for Ludic's named color palette. +# Emits: selfhost/emit_color.ludic (compiler lookup) and palette.json (docs). +import json, sys, os + +# (Name, 0xRRGGBB, group). Names are PascalCase, unique. >200 entries. +PALETTE = [ + # ---- Whites & off-whites ---- + ("White", 0xFFFFFF, "Whites"), + ("Snow", 0xFFFAFA, "Whites"), + ("Ivory", 0xFFFFF0, "Whites"), + ("EggShellWhite", 0xF0EAD6, "Whites"), + ("FloralWhite", 0xFFFAF0, "Whites"), + ("SeaShell", 0xFFF5EE, "Whites"), + ("Linen", 0xFAF0E6, "Whites"), + ("AntiqueWhite", 0xFAEBD7, "Whites"), + ("OldLace", 0xFDF5E6, "Whites"), + ("Beige", 0xF5F5DC, "Whites"), + ("Cream", 0xFFFDD0, "Whites"), + ("Honeydew", 0xF0FFF0, "Whites"), + ("MintCream", 0xF5FFFA, "Whites"), + ("Azure", 0xF0FFFF, "Whites"), + ("AliceBlue", 0xF0F8FF, "Whites"), + ("GhostWhite", 0xF8F8FF, "Whites"), + ("WhiteSmoke", 0xF5F5F5, "Whites"), + ("Lavender", 0xE6E6FA, "Whites"), + ("Bone", 0xE3DAC9, "Whites"), + ("Parchment", 0xF1E9D2, "Whites"), + + # ---- Grays & neutrals ---- + ("Gainsboro", 0xDCDCDC, "Grays"), + ("LightGray", 0xD3D3D3, "Grays"), + ("Silver", 0xC0C0C0, "Grays"), + ("Ash", 0xB2BEB5, "Grays"), + ("DarkGray", 0xA9A9A9, "Grays"), + ("Gray", 0x808080, "Grays"), + ("DimGray", 0x696969, "Grays"), + ("Nickel", 0x727472, "Grays"), + ("Slate", 0x708090, "Grays"), + ("SlateGray", 0x708090, "Grays"), + ("LightSlateGray", 0x778899, "Grays"), + ("Gunmetal", 0x2A3439, "Grays"), + ("Charcoal", 0x36454F, "Grays"), + ("Graphite", 0x1C1C1C, "Grays"), + ("Onyx", 0x353839, "Grays"), + ("Jet", 0x343434, "Grays"), + ("Black", 0x000000, "Grays"), + ("EerieBlack", 0x1B1B1B, "Grays"), + ("RaisinBlack", 0x242124, "Grays"), + ("Ebony", 0x555D50, "Grays"), + + # ---- Reds ---- + ("Red", 0xFF0000, "Reds"), + ("Crimson", 0xDC143C, "Reds"), + ("Scarlet", 0xFF2400, "Reds"), + ("Vermilion", 0xE34234, "Reds"), + ("FireBrick", 0xB22222, "Reds"), + ("Cinnabar", 0xE44D2E, "Reds"), + ("DarkRed", 0x8B0000, "Reds"), + ("Maroon", 0x800000, "Reds"), + ("Ruby", 0xE0115F, "Reds"), + ("Cardinal", 0xC41E3A, "Reds"), + ("IndianRed", 0xCD5C5C, "Reds"), + ("Rust", 0xB7410E, "Reds"), + ("Sangria", 0x92000A, "Reds"), + ("Redwood", 0xA45A52, "Reds"), + ("Cerise", 0xDE3163, "Reds"), + ("Amaranth", 0xE52B50, "Reds"), + ("Carmine", 0x960018, "Reds"), + ("Chestnut", 0x954535, "Reds"), + ("Brick", 0xCB4154, "Reds"), + ("TerraCotta", 0xE2725B, "Reds"), + + # ---- Pinks ---- + ("Pink", 0xFFC0CB, "Pinks"), + ("LightPink", 0xFFB6C1, "Pinks"), + ("HotPink", 0xFF69B4, "Pinks"), + ("DeepPink", 0xFF1493, "Pinks"), + ("PaleVioletRed", 0xDB7093, "Pinks"), + ("Rose", 0xFF007F, "Pinks"), + ("Blush", 0xDE5D83, "Pinks"), + ("Salmon", 0xFA8072, "Pinks"), + ("LightSalmon", 0xFFA07A, "Pinks"), + ("DarkSalmon", 0xE9967A, "Pinks"), + ("Coral", 0xFF7F50, "Pinks"), + ("Watermelon", 0xFC6C85, "Pinks"), + ("Flamingo", 0xFC8EAC, "Pinks"), + ("Bubblegum", 0xFFC1CC, "Pinks"), + ("Fuchsia", 0xFF00FF, "Pinks"), + ("Magenta", 0xFF00FF, "Pinks"), + ("Mauve", 0xE0B0FF, "Pinks"), + ("Puce", 0xCC8899, "Pinks"), + ("Thistle", 0xD8BFD8, "Pinks"), + ("Orchid", 0xDA70D6, "Pinks"), + + # ---- Oranges ---- + ("Orange", 0xFFA500, "Oranges"), + ("DarkOrange", 0xFF8C00, "Oranges"), + ("Tangerine", 0xF28500, "Oranges"), + ("Pumpkin", 0xFF7518, "Oranges"), + ("Apricot", 0xFBCEB1, "Oranges"), + ("Peach", 0xFFE5B4, "Oranges"), + ("Cantaloupe", 0xFFA62B, "Oranges"), + ("Amber", 0xFFBF00, "Oranges"), + ("Bronze", 0xCD7F32, "Oranges"), + ("Copper", 0xB87333, "Oranges"), + ("Marigold", 0xEAA221, "Oranges"), + ("Carrot", 0xED9121, "Oranges"), + ("Persimmon", 0xEC5800, "Oranges"), + ("Papaya", 0xFF9E2C, "Oranges"), + ("Sunset", 0xFAD6A5, "Oranges"), + + # ---- Yellows ---- + ("Yellow", 0xFFFF00, "Yellows"), + ("LightYellow", 0xFFFFE0, "Yellows"), + ("Gold", 0xFFD700, "Yellows"), + ("Goldenrod", 0xDAA520, "Yellows"), + ("Lemon", 0xFFF700, "Yellows"), + ("Canary", 0xFFEF00, "Yellows"), + ("Mustard", 0xFFDB58, "Yellows"), + ("Flax", 0xEEDC82, "Yellows"), + ("Wheat", 0xF5DEB3, "Yellows"), + ("Corn", 0xFBEC5D, "Yellows"), + ("Dandelion", 0xF0E130, "Yellows"), + ("Saffron", 0xF4C430, "Yellows"), + ("Khaki", 0xF0E68C, "Yellows"), + ("DarkKhaki", 0xBDB76B, "Yellows"), + ("Straw", 0xE4D96F, "Yellows"), + + # ---- Browns ---- + ("Brown", 0x8B4513, "Browns"), + ("SaddleBrown", 0x8B4513, "Browns"), + ("Sienna", 0xA0522D, "Browns"), + ("Chocolate", 0xD2691E, "Browns"), + ("Peru", 0xCD853F, "Browns"), + ("Tan", 0xD2B48C, "Browns"), + ("BurlyWood", 0xDEB887, "Browns"), + ("Sand", 0xC2B280, "Browns"), + ("Coffee", 0x6F4E37, "Browns"), + ("Espresso", 0x4B3621, "Browns"), + ("Mahogany", 0xC04000, "Browns"), + ("Walnut", 0x773F1A, "Browns"), + ("Umber", 0x635147, "Browns"), + ("Sepia", 0x704214, "Browns"), + ("Taupe", 0x483C32, "Browns"), + ("Fawn", 0xE5AA70, "Browns"), + ("Caramel", 0xC68E17, "Browns"), + ("Cocoa", 0xD2691E, "Browns"), + ("Hazel", 0x8E7618, "Browns"), + ("Wenge", 0x645452, "Browns"), + + # ---- Greens ---- + ("Green", 0x008000, "Greens"), + ("Lime", 0x00FF00, "Greens"), + ("LimeGreen", 0x32CD32, "Greens"), + ("LawnGreen", 0x7CFC00, "Greens"), + ("Chartreuse", 0x7FFF00, "Greens"), + ("GreenYellow", 0xADFF2F, "Greens"), + ("SpringGreen", 0x00FF7F, "Greens"), + ("MintGreen", 0x98FF98, "Greens"), + ("SeaGreen", 0x2E8B57, "Greens"), + ("MediumSeaGreen", 0x3CB371, "Greens"), + ("ForestGreen", 0x228B22, "Greens"), + ("DarkGreen", 0x006400, "Greens"), + ("OliveDrab", 0x6B8E23, "Greens"), + ("Olive", 0x808000, "Greens"), + ("Moss", 0x8A9A5B, "Greens"), + ("Fern", 0x4F7942, "Greens"), + ("Emerald", 0x50C878, "Greens"), + ("Jade", 0x00A86B, "Greens"), + ("Malachite", 0x0BDA51, "Greens"), + ("Shamrock", 0x009E60, "Greens"), + ("Pistachio", 0x93C572, "Greens"), + ("Avocado", 0x568203, "Greens"), + ("Pine", 0x01796F, "Greens"), + ("Sage", 0x9CAF88, "Greens"), + ("Kelly", 0x4CBB17, "Greens"), + ("Hunter", 0x355E3B, "Greens"), + ("Basil", 0x579229, "Greens"), + ("Clover", 0x2E8B57, "Greens"), + ("Juniper", 0x6D9A79, "Greens"), + ("Neon", 0x39FF14, "Greens"), + + # ---- Cyans / teals ---- + ("Cyan", 0x00FFFF, "Cyans"), + ("Aqua", 0x00FFFF, "Cyans"), + ("LightCyan", 0xE0FFFF, "Cyans"), + ("PaleTurquoise", 0xAFEEEE, "Cyans"), + ("Aquamarine", 0x7FFFD4, "Cyans"), + ("Turquoise", 0x40E0D0, "Cyans"), + ("MediumTurquoise",0x48D1CC, "Cyans"), + ("DarkTurquoise", 0x00CED1, "Cyans"), + ("Teal", 0x008080, "Cyans"), + ("DarkCyan", 0x008B8B, "Cyans"), + ("CadetBlue", 0x5F9EA0, "Cyans"), + ("Lagoon", 0x018E8E, "Cyans"), + ("Seafoam", 0x93E9BE, "Cyans"), + ("Cerulean", 0x007BA7, "Cyans"), + ("SkyBlueLight", 0x80DAEB, "Cyans"), + ("Robin", 0x00CCCC, "Cyans"), + ("Verdigris", 0x43B3AE, "Cyans"), + ("Celadon", 0xACE1AF, "Cyans"), + + # ---- Blues ---- + ("Blue", 0x0000FF, "Blues"), + ("LightBlue", 0xADD8E6, "Blues"), + ("PowderBlue", 0xB0E0E6, "Blues"), + ("SkyBlue", 0x87CEEB, "Blues"), + ("LightSkyBlue", 0x87CEFA, "Blues"), + ("DeepSkyBlue", 0x00BFFF, "Blues"), + ("DodgerBlue", 0x1E90FF, "Blues"), + ("CornflowerBlue", 0x6495ED, "Blues"), + ("SteelBlue", 0x4682B4, "Blues"), + ("RoyalBlue", 0x4169E1, "Blues"), + ("MediumBlue", 0x0000CD, "Blues"), + ("DarkBlue", 0x00008B, "Blues"), + ("Navy", 0x000080, "Blues"), + ("MidnightBlue", 0x191970, "Blues"), + ("Cobalt", 0x0047AB, "Blues"), + ("Sapphire", 0x0F52BA, "Blues"), + ("Denim", 0x1560BD, "Blues"), + ("Indigo", 0x4B0082, "Blues"), + ("Prussian", 0x003153, "Blues"), + ("Ultramarine", 0x3F00FF, "Blues"), + ("Periwinkle", 0xCCCCFF, "Blues"), + ("Iris", 0x5A4FCF, "Blues"), + ("Glaucous", 0x6082B6, "Blues"), + ("Zaffre", 0x0014A8, "Blues"), + ("Berry", 0x2E2D88, "Blues"), + + # ---- Purples ---- + ("Purple", 0x800080, "Purples"), + ("Violet", 0xEE82EE, "Purples"), + ("DarkViolet", 0x9400D3, "Purples"), + ("BlueViolet", 0x8A2BE2, "Purples"), + ("MediumPurple", 0x9370DB, "Purples"), + ("Amethyst", 0x9966CC, "Purples"), + ("Plum", 0x8E4585, "Purples"), + ("Eggplant", 0x614051, "Purples"), + ("Grape", 0x6F2DA8, "Purples"), + ("Wine", 0x722F37, "Purples"), + ("Mulberry", 0xC54B8C, "Purples"), + ("Lilac", 0xC8A2C8, "Purples"), + ("Wisteria", 0xC9A0DC, "Purples"), + ("Heliotrope", 0xDF73FF, "Purples"), + ("Byzantium", 0x702963, "Purples"), + ("Tyrian", 0x66023C, "Purples"), + ("RebeccaPurple", 0x663399, "Purples"), + ("Orchid2", 0xAF69EF, "Purples"), +] + +def guard_check(): + names = [p[0] for p in PALETTE] + dup = set(n for n in names if names.count(n) > 1) + if dup: + print("DUPLICATE NAMES:", dup, file=sys.stderr); sys.exit(1) + return names + +def emit_ludic(path): + lines = [] + lines.append("# ============================================================================") + lines.append("# emit_color.ludic — the named-color palette, resolved at compile time.") + lines.append("#") + lines.append("# `Color.Name` in a game lowers to a plain 0xRRGGBB int here: no runtime cost,") + lines.append("# no allocation, identical codegen to writing the hex by hand. Unknown names are") + lines.append("# a compile error (color_lookup returns -1, which emit_expr reports).") + lines.append("#") + lines.append("# GENERATED by scratchpad/palette.py from the single source-of-truth palette.") + lines.append("# Edit the palette there and regenerate; do not hand-edit this file.") + lines.append("# ============================================================================") + lines.append("") + lines.append("fn color_lookup(name: ptr) -> int {") + for (nm, hexv, grp) in PALETTE: + lines.append(f' if (name == "{nm}") {{ return 0x{hexv:06X} }}') + lines.append(" return -1") + lines.append("}") + lines.append("") + with open(path, "w") as f: + f.write("\n".join(lines) + "\n") + +def emit_json(path): + groups = {} + order = [] + for (nm, hexv, grp) in PALETTE: + if grp not in groups: + groups[grp] = []; order.append(grp) + groups[grp].append({"name": nm, "hex": f"{hexv:06X}"}) + out = {"count": len(PALETTE), "groups": [{"name": g, "colors": groups[g]} for g in order]} + with open(path, "w") as f: + json.dump(out, f, indent=2) + +if __name__ == "__main__": + names = guard_check() + root = os.path.dirname(os.path.abspath(__file__)) + repo = "/Users/orkuncakilkaya/workspace/gpp" + emit_ludic(os.path.join(repo, "selfhost", "emit_color.ludic")) + emit_json(os.path.join(root, "palette.json")) + print(f"OK {len(PALETTE)} colors ({len(set(names))} unique names)")