A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Further website updates and syntax fixes

+507 -210
+3 -2
SPEC.md
··· 118 118 null // Null value 119 119 boolean // True or false 120 120 integer // 64-bit integer 121 - number // Double-precision float 122 121 string // UTF-8 string 123 122 bytes // Byte array 124 123 ``` 124 + 125 + **Note:** ATProto Lexicons do not support floating-point numbers. Only `integer` is available for numeric values. 125 126 126 127 ### Special String Formats 127 128 ··· 914 915 915 916 ``` 916 917 as, blob, boolean, bytes, constrained, def, error, inline, integer, 917 - null, number, procedure, query, record, string, subscription, token, 918 + null, procedure, query, record, string, subscription, token, 918 919 type, unknown, use 919 920 ``` 920 921
-1
mlf-codegen/src/lib.rs
··· 461 461 PrimitiveType::Null => json!({ "type": "null" }), 462 462 PrimitiveType::Boolean => json!({ "type": "boolean" }), 463 463 PrimitiveType::Integer => json!({ "type": "integer" }), 464 - PrimitiveType::Number => json!({ "type": "number" }), 465 464 PrimitiveType::String => json!({ "type": "string" }), 466 465 PrimitiveType::Bytes => json!({ "type": "bytes" }), 467 466 PrimitiveType::Blob => json!({ "type": "blob" }),
-1
mlf-lang/src/ast.rs
··· 285 285 Null, 286 286 Boolean, 287 287 Integer, 288 - Number, 289 288 String, 290 289 Bytes, 291 290 Blob,
-3
mlf-lang/src/lexer.rs
··· 26 26 Integer, 27 27 Namespace, 28 28 Null, 29 - Number, 30 29 Procedure, 31 30 Query, 32 31 Record, ··· 81 80 Token::Integer => write!(f, "integer"), 82 81 Token::Namespace => write!(f, "namespace"), 83 82 Token::Null => write!(f, "null"), 84 - Token::Number => write!(f, "number"), 85 83 Token::Procedure => write!(f, "procedure"), 86 84 Token::Query => write!(f, "query"), 87 85 Token::Record => write!(f, "record"), ··· 155 153 "integer" => Token::Integer, 156 154 "namespace" => Token::Namespace, 157 155 "null" => Token::Null, 158 - "number" => Token::Number, 159 156 "procedure" => Token::Procedure, 160 157 "query" => Token::Query, 161 158 "record" => Token::Record,
-8
mlf-lang/src/parser.rs
··· 79 79 LexToken::As => "as".into(), 80 80 LexToken::String => "string".into(), 81 81 LexToken::Integer => "integer".into(), 82 - LexToken::Number => "number".into(), 83 82 LexToken::Boolean => "boolean".into(), 84 83 LexToken::Null => "null".into(), 85 84 LexToken::Unknown => "unknown".into(), ··· 655 654 let span = self.advance().span; 656 655 Type::Primitive { 657 656 kind: PrimitiveType::Integer, 658 - span, 659 - } 660 - } 661 - LexToken::Number => { 662 - let span = self.advance().span; 663 - Type::Primitive { 664 - kind: PrimitiveType::Number, 665 657 span, 666 658 } 667 659 }
+1 -1
mlf-lang/src/workspace.rs
··· 261 261 } 262 262 Constraint::Minimum { span, .. } 263 263 | Constraint::Maximum { span, .. } => { 264 - if !matches!(base_kind, Some(PrimitiveType::Integer) | Some(PrimitiveType::Number)) { 264 + if !matches!(base_kind, Some(PrimitiveType::Integer)) { 265 265 errors.push(ValidationError::InvalidConstraint { 266 266 message: alloc::format!("Numeric constraint on non-numeric type"), 267 267 span: *span,
-8
mlf-validation/src/lib.rs
··· 173 173 }); 174 174 } 175 175 } 176 - PrimitiveType::Number => { 177 - if !value.is_f64() && !value.is_i64() { 178 - errors.push(ValidationError { 179 - path: path.to_string(), 180 - message: "Expected number".to_string(), 181 - }); 182 - } 183 - } 184 176 PrimitiveType::String => { 185 177 if !value.is_string() { 186 178 errors.push(ValidationError {
+12 -7
tree-sitter-mlf/grammar.js
··· 16 16 $.comment, 17 17 ], 18 18 19 + conflicts: $ => [ 20 + [$.type, $.union_type] 21 + ], 22 + 19 23 rules: { 20 24 source_file: $ => repeat($.item), 21 25 ··· 95 99 'query', 96 100 field('name', $.identifier), 97 101 field('params', $.parameter_list), 102 + ':', 98 103 field('return', $.return_type), 99 104 ';' 100 105 ), ··· 104 109 'procedure', 105 110 field('name', $.identifier), 106 111 field('params', $.parameter_list), 112 + ':', 107 113 field('return', $.return_type), 108 114 ';' 109 115 ), ··· 113 119 'subscription', 114 120 field('name', $.identifier), 115 121 field('params', $.parameter_list), 116 - '->', 122 + ':', 117 123 field('messages', $.type), 118 124 ';' 119 125 ), ··· 136 142 ), 137 143 138 144 return_type: $ => choice( 139 - seq('->', $.type), 140 - seq( 141 - '->', 145 + prec(1, seq( 142 146 $.type, 143 - 'throws', 147 + '|', 148 + 'error', 144 149 '{', 145 150 repeat($.error_definition), 146 151 '}' 147 - ) 152 + )), 153 + $.type 148 154 ), 149 155 150 156 error_definition: $ => seq( ··· 171 177 'null', 172 178 'boolean', 173 179 'integer', 174 - 'number', 175 180 'string', 176 181 'bytes', 177 182 'blob',
+9 -7
tree-sitter-mlf/queries/highlights.scm
··· 1 1 ; Keywords 2 2 [ 3 - "namespace" 4 3 "use" 5 4 "record" 6 - "alias" 5 + "def" 6 + "inline" 7 + "type" 7 8 "token" 8 9 "query" 9 10 "procedure" 10 11 "subscription" 11 - "throws" 12 + "error" 12 13 "constrained" 13 14 ] @keyword 14 15 ··· 17 18 "null" 18 19 "boolean" 19 20 "integer" 20 - "number" 21 21 "string" 22 22 "bytes" 23 23 "blob" ··· 42 42 (record_definition 43 43 name: (identifier) @type) 44 44 45 - ; Alias names 46 - (alias_definition 45 + ; Type definition names 46 + (def_type_definition 47 + name: (identifier) @type) 48 + 49 + (inline_type_definition 47 50 name: (identifier) @type) 48 51 49 52 ; Token names ··· 75 78 [ 76 79 ":" 77 80 "=" 78 - "->" 79 81 "|" 80 82 "?" 81 83 ] @operator
+51 -80
tree-sitter-mlf/src/grammar.json
··· 13 13 "members": [ 14 14 { 15 15 "type": "SYMBOL", 16 - "name": "namespace_declaration" 17 - }, 18 - { 19 - "type": "SYMBOL", 20 16 "name": "use_statement" 21 17 }, 22 18 { ··· 81 77 ] 82 78 } 83 79 }, 84 - "namespace_declaration": { 85 - "type": "SEQ", 86 - "members": [ 87 - { 88 - "type": "STRING", 89 - "value": "namespace" 90 - }, 91 - { 92 - "type": "FIELD", 93 - "name": "name", 94 - "content": { 95 - "type": "SYMBOL", 96 - "name": "namespace_identifier" 97 - } 98 - }, 99 - { 100 - "type": "STRING", 101 - "value": ";" 102 - } 103 - ] 104 - }, 105 - "namespace_identifier": { 106 - "type": "PATTERN", 107 - "value": "[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)*" 108 - }, 109 80 "use_statement": { 110 81 "type": "SEQ", 111 82 "members": [ ··· 149 120 "type": "SYMBOL", 150 121 "name": "record_body" 151 122 } 152 - }, 153 - { 154 - "type": "STRING", 155 - "value": ";" 156 123 } 157 124 ] 158 125 }, ··· 348 315 } 349 316 }, 350 317 { 318 + "type": "STRING", 319 + "value": ":" 320 + }, 321 + { 351 322 "type": "FIELD", 352 323 "name": "return", 353 324 "content": { ··· 385 356 } 386 357 }, 387 358 { 359 + "type": "STRING", 360 + "value": ":" 361 + }, 362 + { 388 363 "type": "FIELD", 389 364 "name": "return", 390 365 "content": { ··· 423 398 }, 424 399 { 425 400 "type": "STRING", 426 - "value": "->" 401 + "value": ":" 427 402 }, 428 403 { 429 404 "type": "FIELD", ··· 538 513 "type": "CHOICE", 539 514 "members": [ 540 515 { 541 - "type": "SEQ", 542 - "members": [ 543 - { 544 - "type": "STRING", 545 - "value": "->" 546 - }, 547 - { 548 - "type": "SYMBOL", 549 - "name": "type" 550 - } 551 - ] 516 + "type": "PREC", 517 + "value": 1, 518 + "content": { 519 + "type": "SEQ", 520 + "members": [ 521 + { 522 + "type": "SYMBOL", 523 + "name": "type" 524 + }, 525 + { 526 + "type": "STRING", 527 + "value": "|" 528 + }, 529 + { 530 + "type": "STRING", 531 + "value": "error" 532 + }, 533 + { 534 + "type": "STRING", 535 + "value": "{" 536 + }, 537 + { 538 + "type": "REPEAT", 539 + "content": { 540 + "type": "SYMBOL", 541 + "name": "error_definition" 542 + } 543 + }, 544 + { 545 + "type": "STRING", 546 + "value": "}" 547 + } 548 + ] 549 + } 552 550 }, 553 551 { 554 - "type": "SEQ", 555 - "members": [ 556 - { 557 - "type": "STRING", 558 - "value": "->" 559 - }, 560 - { 561 - "type": "SYMBOL", 562 - "name": "type" 563 - }, 564 - { 565 - "type": "STRING", 566 - "value": "throws" 567 - }, 568 - { 569 - "type": "STRING", 570 - "value": "{" 571 - }, 572 - { 573 - "type": "REPEAT", 574 - "content": { 575 - "type": "SYMBOL", 576 - "name": "error_definition" 577 - } 578 - }, 579 - { 580 - "type": "STRING", 581 - "value": "}" 582 - } 583 - ] 552 + "type": "SYMBOL", 553 + "name": "type" 584 554 } 585 555 ] 586 556 }, ··· 665 635 { 666 636 "type": "STRING", 667 637 "value": "integer" 668 - }, 669 - { 670 - "type": "STRING", 671 - "value": "number" 672 638 }, 673 639 { 674 640 "type": "STRING", ··· 1030 996 "name": "comment" 1031 997 } 1032 998 ], 1033 - "conflicts": [], 999 + "conflicts": [ 1000 + [ 1001 + "type", 1002 + "union_type" 1003 + ] 1004 + ], 1034 1005 "precedences": [], 1035 1006 "externals": [], 1036 1007 "inline": [],
+4 -40
tree-sitter-mlf/src/node-types.json
··· 265 265 "named": true 266 266 }, 267 267 { 268 - "type": "namespace_declaration", 269 - "named": true 270 - }, 271 - { 272 268 "type": "procedure_definition", 273 269 "named": true 274 270 }, ··· 293 289 "named": true 294 290 } 295 291 ] 296 - } 297 - }, 298 - { 299 - "type": "namespace_declaration", 300 - "named": true, 301 - "fields": { 302 - "name": { 303 - "multiple": false, 304 - "required": true, 305 - "types": [ 306 - { 307 - "type": "namespace_identifier", 308 - "named": true 309 - } 310 - ] 311 - } 312 292 } 313 293 }, 314 294 { ··· 695 675 "named": false 696 676 }, 697 677 { 698 - "type": "->", 699 - "named": false 700 - }, 701 - { 702 678 "type": ".", 703 679 "named": false 704 680 }, ··· 759 735 "named": true 760 736 }, 761 737 { 738 + "type": "error", 739 + "named": false 740 + }, 741 + { 762 742 "type": "false", 763 743 "named": false 764 744 }, ··· 771 751 "named": false 772 752 }, 773 753 { 774 - "type": "namespace", 775 - "named": false 776 - }, 777 - { 778 - "type": "namespace_identifier", 779 - "named": true 780 - }, 781 - { 782 754 "type": "null", 783 755 "named": false 784 756 }, 785 757 { 786 758 "type": "number", 787 759 "named": true 788 - }, 789 - { 790 - "type": "number", 791 - "named": false 792 760 }, 793 761 { 794 762 "type": "procedure", ··· 812 780 }, 813 781 { 814 782 "type": "subscription", 815 - "named": false 816 - }, 817 - { 818 - "type": "throws", 819 783 "named": false 820 784 }, 821 785 {
+5 -11
website/content/docs/language-guide/02-fields.md
··· 37 37 } 38 38 ``` 39 39 40 - **Numbers** (double-precision floats): 41 - ```mlf 42 - record example { 43 - price: number, 44 - rating: number, 45 - } 46 - ``` 40 + **Note:** ATProto Lexicons only support integers for numeric values. There is no float/double type. 47 41 48 42 **Booleans:** 49 43 ```mlf ··· 114 108 location: { 115 109 city: string, 116 110 coordinates: { 117 - lat: number, 118 - lng: number, 111 + lat: integer, 112 + lng: integer, 119 113 }, 120 114 }, 121 115 } ··· 178 172 /// Optional geographic location 179 173 location?: { 180 174 name: string, 181 - lat: number, 182 - lng: number, 175 + lat: integer, 176 + lng: integer, 183 177 }, 184 178 185 179 /// Tags on this post
+5 -19
website/content/docs/language-guide/03-constraints.md
··· 93 93 } 94 94 ``` 95 95 96 - ## Number Constraints 97 - 98 - Numbers (floats) work the same as integers: 99 - 100 - ```mlf 101 - rating: number constrained { 102 - minimum: 0.0, 103 - maximum: 5.0, 104 - } 105 - 106 - price: number constrained { 107 - minimum: 0.0, 108 - default: 0.0, 109 - } 110 - ``` 96 + **Note:** ATProto Lexicons do not support a `number`/`float` type. Only `integer` is supported for numeric values. 111 97 112 98 ## Boolean Constraints 113 99 ··· 238 224 maxSize: 500000, 239 225 }, 240 226 241 - /// Average rating (0-5 stars) 242 - rating?: number constrained { 243 - minimum: 0.0, 244 - maximum: 5.0, 227 + /// Average rating (0-5 scale) 228 + rating?: integer constrained { 229 + minimum: 0, 230 + maximum: 5, 245 231 }, 246 232 247 233 /// Whether thread is featured
+2 -2
website/content/docs/language-guide/04-custom-types.md
··· 62 62 63 63 ```mlf 64 64 inline type Coordinates = { 65 - lat: number, 66 - lng: number, 65 + lat: integer, 66 + lng: integer, 67 67 }; 68 68 69 69 record location {
+1 -1
website/content/docs/language-guide/08-imports.md
··· 223 223 224 224 ## What's Next? 225 225 226 - Finally, let's learn about the prelude - built-in types available in every file. 226 + Next, let's learn about the prelude - built-in types available in every file. After that, we'll cover important information about how MLF maps to ATProto Lexicons.
+5 -1
website/content/docs/language-guide/09-prelude.md
··· 193 193 194 194 You've now learned all the core features of MLF! You can define records, add constraints, create custom types, use unions and tokens, define XRPC operations, import from other files, and use prelude types. 195 195 196 - Check out the [Playground](/playground/) to experiment with MLF, or read the [CLI documentation](/docs/cli/) to learn how to compile your lexicons. 196 + ## What's Next? 197 + 198 + Read the [Important Info](/docs/language-guide/10-important-info/) section to understand how MLF maps to ATProto Lexicons, especially the rules for the `"main"` definition. 199 + 200 + Then check out the [Playground](/playground/) to experiment with MLF, or read the [CLI documentation](/docs/cli/) to learn how to compile your lexicons.
+220
website/content/docs/language-guide/10-important-info.md
··· 1 + +++ 2 + title = "Important Info" 3 + weight = 10 4 + +++ 5 + 6 + This section covers important details about how MLF maps to ATProto Lexicons. 7 + 8 + ## The "main" Definition 9 + 10 + In ATProto Lexicons, each lexicon has a `defs` object where definitions are stored. One special definition is called `"main"` - it's the primary definition for that lexicon. 11 + 12 + ### When Does a Definition Become "main"? 13 + 14 + MLF automatically determines which definition becomes `"main"` based on two rules: 15 + 16 + **Rule 1: Single Main-Eligible Item** 17 + 18 + If your file contains **exactly one** record, query, procedure, or subscription, it automatically becomes `"main"`: 19 + 20 + ```mlf 21 + // File: com/example/forum/post.mlf 22 + record post { 23 + text: string, 24 + author: Did, 25 + } 26 + ``` 27 + 28 + This generates: 29 + ```json 30 + { 31 + "lexicon": 1, 32 + "id": "com.example.forum.post", 33 + "defs": { 34 + "main": { 35 + "type": "record", 36 + ... 37 + } 38 + } 39 + } 40 + ``` 41 + 42 + **Rule 2: Name Matches NSID Fragment** 43 + 44 + If your definition name matches the **last segment** of the NSID, it becomes `"main"`: 45 + 46 + ```mlf 47 + // File: com/example/forum/post.mlf 48 + record post { // Name "post" matches last NSID segment 49 + text: string, 50 + } 51 + 52 + def type author = { 53 + did: Did, 54 + }; 55 + ``` 56 + 57 + The `post` record becomes `"main"`, while `author` becomes a named def: 58 + 59 + ```json 60 + { 61 + "lexicon": 1, 62 + "id": "com.example.forum.post", 63 + "defs": { 64 + "main": { 65 + "type": "record", 66 + ... 67 + }, 68 + "author": { 69 + "type": "object", 70 + ... 71 + } 72 + } 73 + } 74 + ``` 75 + 76 + ### Multiple Main-Eligible Items 77 + 78 + If you have multiple main-eligible items and none match the NSID fragment, they all become named defs: 79 + 80 + ```mlf 81 + // File: com/example/forum/thread.mlf 82 + record post { // Doesn't match "thread" 83 + text: string, 84 + } 85 + 86 + record reply { // Doesn't match "thread" 87 + text: string, 88 + } 89 + ``` 90 + 91 + Generates: 92 + ```json 93 + { 94 + "lexicon": 1, 95 + "id": "com.example.forum.thread", 96 + "defs": { 97 + "post": { ... }, 98 + "reply": { ... } 99 + } 100 + } 101 + ``` 102 + 103 + **Note:** Neither becomes `"main"` because the file is named `thread.mlf` but contains `post` and `reply`. 104 + 105 + ### Supporting Definitions 106 + 107 + These are **never** `"main"` - they're always named defs: 108 + - `def type` definitions 109 + - `token` definitions 110 + - `inline type` definitions (don't appear in output at all) 111 + 112 + ### Best Practices 113 + 114 + **For single-definition files:** 115 + ```mlf 116 + // File: com/example/forum/post.mlf 117 + record post { 118 + // The record name matches the filename 119 + text: string, 120 + } 121 + ``` 122 + 123 + **For multi-definition files:** 124 + ```mlf 125 + // File: com/example/forum/post.mlf 126 + record post { // Becomes "main" (matches NSID) 127 + text: string, 128 + author: author, 129 + } 130 + 131 + def type author = { // Named def: "author" 132 + did: Did, 133 + handle: Handle, 134 + }; 135 + 136 + token draft; // Named def: "draft" 137 + token published; // Named def: "published" 138 + ``` 139 + 140 + **For shared types files:** 141 + ```mlf 142 + // File: com/example/forum/defs.mlf 143 + def type author = { 144 + did: Did, 145 + }; 146 + 147 + def type postRef = { 148 + uri: AtUri, 149 + }; 150 + ``` 151 + 152 + When the NSID ends with `defs`, all items become named defs (no `"main"`). 153 + 154 + ## NSID and File Path Mapping 155 + 156 + The file path **is** the NSID. MLF derives the lexicon NSID from the file path: 157 + 158 + | File Path | NSID | Last Segment | 159 + |-----------|------|--------------| 160 + | `com/example/forum/post.mlf` | `com.example.forum.post` | `post` | 161 + | `com/example/forum/thread.mlf` | `com.example.forum.thread` | `thread` | 162 + | `com/example/forum/defs.mlf` | `com.example.forum.defs` | `defs` | 163 + 164 + The last segment determines which definition becomes `"main"` (when using Rule 2). 165 + 166 + ## Reserved Names 167 + 168 + You **cannot** use these as definition names under any circumstances: 169 + - `main` - Reserved for the main definition 170 + - `defs` - Reserved for the definitions container 171 + 172 + MLF will reject these names even with raw identifiers (backticks). 173 + 174 + ## Reserved Keywords and Raw Identifiers 175 + 176 + MLF has reserved keywords that normally cannot be used as field or type names: 177 + 178 + ``` 179 + as, blob, boolean, bytes, constrained, def, error, inline, integer, 180 + null, procedure, query, record, string, subscription, token, 181 + type, unknown, use 182 + ``` 183 + 184 + **Note:** The `number` type is not supported by ATProto Lexicons. Only `integer` is available for numeric values. 185 + 186 + However, you can use keywords as identifiers by wrapping them in **backticks** (`` ` ``): 187 + 188 + ```mlf 189 + def type metadata = { 190 + `type`: string, // "type" is a keyword, escaped with backticks 191 + `record`: AtUri, // "record" is a keyword, escaped 192 + `error`: string, // "error" is a keyword, escaped 193 + name: string, // "name" is not a keyword, no escaping needed 194 + }; 195 + ``` 196 + 197 + This is useful for compatibility with existing ATProto schemas that use these names. 198 + 199 + **Type names can also be escaped:** 200 + ```mlf 201 + def type `record` = { // Type name "record" escaped 202 + uri: AtUri, 203 + cid: Cid, 204 + }; 205 + ``` 206 + 207 + **When you need raw identifiers:** 208 + - Importing existing ATProto lexicons that use keyword names 209 + - Maintaining compatibility with JSON schemas 210 + - Working with established conventions in the ATProto ecosystem 211 + 212 + **Important:** This only works for field names and type names. You cannot escape `main` or `defs` - they are always forbidden. 213 + 214 + ## Summary 215 + 216 + - **Single main-eligible item** → automatically becomes `"main"` 217 + - **Name matches last NSID segment** → becomes `"main"` 218 + - **Neither condition met** → all items become named defs 219 + - **Supporting definitions** (def type, token) → always named defs 220 + - **File path** → determines the NSID
+156 -13
website/sass/style.scss
··· 680 680 font-size: 0.875rem; 681 681 } 682 682 683 + /* Mobile menu toggle */ 684 + .mobile-menu-toggle { 685 + display: none; 686 + background: none; 687 + border: none; 688 + color: var(--text); 689 + font-size: 1.5rem; 690 + cursor: pointer; 691 + padding: 0.5rem; 692 + line-height: 1; 693 + } 694 + 695 + .mobile-menu-toggle:hover { 696 + color: var(--accent); 697 + } 698 + 683 699 /* Responsive */ 684 700 @media (max-width: 768px) { 685 701 .hero h1 { ··· 699 715 grid-template-columns: 1fr; 700 716 } 701 717 718 + /* Mobile navigation */ 719 + .mobile-menu-toggle { 720 + display: block; 721 + } 722 + 702 723 .nav-links { 703 - gap: 1rem; 704 - font-size: 0.875rem; 724 + display: none; 725 + position: absolute; 726 + top: 100%; 727 + left: 0; 728 + right: 0; 729 + background: var(--bg-elevated); 730 + flex-direction: column; 731 + gap: 0; 732 + padding: 1rem 0; 733 + border-bottom: 1px solid var(--border); 734 + z-index: 50; 735 + } 736 + 737 + .nav-links.active { 738 + display: flex; 739 + } 740 + 741 + .nav-links li { 742 + width: 100%; 743 + } 744 + 745 + .nav-links a { 746 + display: block; 747 + padding: 0.75rem 1.5rem; 748 + font-size: 1rem; 749 + } 750 + 751 + nav { 752 + position: relative; 705 753 } 706 754 707 755 .cta-buttons { ··· 717 765 718 766 .doc-layout { 719 767 grid-template-columns: 1fr; 720 - gap: 2rem; 768 + gap: 1.5rem; 769 + } 770 + 771 + .doc-page { 772 + padding: 1.5rem 0; 721 773 } 722 774 723 775 .doc-sidebar { 724 776 position: static; 725 - border-bottom: 1px solid var(--border); 726 - padding-bottom: 1.5rem; 777 + border: 1px solid var(--border); 778 + border-radius: 0.375rem; 779 + padding: 1rem; 727 780 margin-bottom: 1.5rem; 728 781 } 729 782 730 - .doc-nav ul { 731 - display: flex; 732 - flex-wrap: wrap; 733 - gap: 0.5rem; 783 + .doc-nav h3 { 784 + cursor: pointer; 785 + user-select: none; 786 + } 787 + 788 + .doc-nav h3::after { 789 + content: " ▼"; 790 + font-size: 0.75rem; 791 + color: var(--text-muted); 792 + } 793 + 794 + .doc-nav > ul { 795 + max-height: 500px; 796 + overflow-y: auto; 797 + } 798 + 799 + .doc-nav .nav-section ul { 800 + padding-left: 0.5rem; 801 + } 802 + 803 + /* Make tables responsive */ 804 + .doc-content table { 805 + display: block; 806 + overflow-x: auto; 807 + white-space: nowrap; 734 808 } 735 809 736 - .doc-nav li { 737 - margin-bottom: 0; 810 + /* Code blocks */ 811 + .doc-content pre { 812 + margin-left: -1.5rem; 813 + margin-right: -1.5rem; 814 + border-radius: 0; 738 815 } 739 816 } 740 817 ··· 751 828 font-size: 1.75rem; 752 829 } 753 830 754 - .nav-links { 755 - gap: 0.75rem; 831 + .nav-container { 832 + padding: 0 1rem; 833 + } 834 + 835 + .logo img { 836 + height: 36px; 756 837 } 757 838 758 839 textarea { 759 840 height: 300px; 841 + } 842 + 843 + .doc-content { 844 + font-size: 0.938rem; 845 + } 846 + 847 + .doc-content h1 { 848 + font-size: 1.875rem; 849 + } 850 + 851 + .doc-content h2 { 852 + font-size: 1.5rem; 853 + } 854 + 855 + .doc-content h3 { 856 + font-size: 1.25rem; 857 + } 858 + 859 + .doc-content pre { 860 + font-size: 0.813rem; 861 + } 862 + 863 + .panel-header { 864 + flex-direction: column; 865 + align-items: flex-start; 866 + } 867 + 868 + .file-path-container { 869 + width: 100%; 870 + } 871 + 872 + .tabs { 873 + width: 100%; 874 + justify-content: space-between; 760 875 } 761 876 } 762 877 ··· 925 1040 padding-left: 1rem; 926 1041 border-left: 3px solid var(--accent); 927 1042 color: var(--text-light); 1043 + } 1044 + 1045 + .doc-content table { 1046 + width: 100%; 1047 + margin: 1.5rem 0; 1048 + border-collapse: collapse; 1049 + font-size: 0.875rem; 1050 + } 1051 + 1052 + .doc-content table th, 1053 + .doc-content table td { 1054 + padding: 0.75rem; 1055 + text-align: left; 1056 + border: 1px solid var(--border); 1057 + } 1058 + 1059 + .doc-content table th { 1060 + background: var(--bg-alt); 1061 + font-weight: 600; 1062 + color: var(--text); 1063 + } 1064 + 1065 + .doc-content table td { 1066 + color: var(--text-light); 1067 + } 1068 + 1069 + .doc-content table code { 1070 + font-size: 0.813rem; 928 1071 } 929 1072 930 1073 .logo a {
+1 -1
website/static/js/app.js
··· 47 47 patterns: [ 48 48 { 49 49 name: 'storage.type.builtin.mlf', 50 - match: '\\b(null|boolean|integer|number|string|bytes|blob|unknown)\\b' 50 + match: '\\b(null|boolean|integer|string|bytes|blob|unknown)\\b' 51 51 }, 52 52 { 53 53 name: 'storage.type.format.mlf',
+1 -3
website/syntaxes/mlf.sublime-syntax
··· 36 36 37 37 types: 38 38 # Primitive types 39 - - match: '\b(null|boolean|integer|number|string|bytes|blob|unknown)\b' 39 + - match: '\b(null|boolean|integer|string|bytes|blob|unknown)\b' 40 40 scope: storage.type.builtin.mlf 41 41 42 42 # Format types ··· 73 73 scope: keyword.operator.union.mlf 74 74 - match: '=' 75 75 scope: keyword.operator.assignment.mlf 76 - - match: '->' 77 - scope: keyword.operator.arrow.mlf
+31 -1
website/templates/base.html
··· 21 21 <img src="{{ get_url(path='logo.svg') }}" alt="MLF" height="48"> 22 22 </a> 23 23 </div> 24 - <ul class="nav-links"> 24 + <button class="mobile-menu-toggle" aria-label="Toggle menu" id="mobile-menu-toggle"> 25 + 26 + </button> 27 + <ul class="nav-links" id="mobile-menu"> 25 28 <li><a href="{{ get_url(path='/') }}">Home</a></li> 26 29 <li><a href="{{ get_url(path='/docs') }}">Docs</a></li> 27 30 <li><a href="{{ get_url(path='@/playground.md') }}">Playground</a></li> ··· 45 48 <p>MLF is licensed under <a href="https://choosealicense.com/licenses/mit/" target="_blank">MIT</a></p> 46 49 </div> 47 50 </footer> 51 + 52 + <script> 53 + // Mobile menu toggle 54 + const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); 55 + const mobileMenu = document.getElementById('mobile-menu'); 56 + 57 + if (mobileMenuToggle && mobileMenu) { 58 + mobileMenuToggle.addEventListener('click', () => { 59 + mobileMenu.classList.toggle('active'); 60 + }); 61 + 62 + // Close menu when clicking a link 63 + const menuLinks = mobileMenu.querySelectorAll('a'); 64 + menuLinks.forEach(link => { 65 + link.addEventListener('click', () => { 66 + mobileMenu.classList.remove('active'); 67 + }); 68 + }); 69 + 70 + // Close menu when clicking outside 71 + document.addEventListener('click', (e) => { 72 + if (!mobileMenuToggle.contains(e.target) && !mobileMenu.contains(e.target)) { 73 + mobileMenu.classList.remove('active'); 74 + } 75 + }); 76 + } 77 + </script> 48 78 49 79 {% block extra_scripts %}{% endblock %} 50 80 </body>