moo

repository·main·Indexed 21 days ago

https://github.com/no-context/moo

A highly-optimized, dependency-free tokenizer/lexer generator for JavaScript (version 0.5.2). It uses regular expressions compiled into a single RegExp and leverages the ES6 sticky flag for performance. Moo supports stateless lexers via moo.compile() and stateful lexers via moo.states(), allowing for context-sensitive tokenization with push, pop, and next transitions. It includes features for tracking line and column positions, transforming token values, and managing keywords using the moo.keywords() helper.

Tokens
3.4K
Snippets
14
Records
16
Agent score
25%

What's inside moo

  1. Define keywords using moo.keywords()

    main

    To ensure the 'longest match' principle (e.g., preventing className from being lexed as class + Name), use the moo.keywords() helper. This helper checks matches against a list of keywords and assigns them a specific type if they match, otherwise falling back to the base type of the rule.

    Individual Keyword Types: You can also map specific keywords to unique types by passing an object to moo.keywords() instead of an array.

    // Using an array for shared type 'keyword'
    moo.compile({
      IDEN: {match: /[a-zA-Z]+/, type: moo.keywords(['while', 'if', 'else'])}
    })
    
    // Using an object for individual types
    moo.compile({
      name: {match: /[a-zA-Z]+/, type: moo.keywords({
        'kw-class': 'class',
        'kw-def': 'def',
      })}
    })
  2. Configure token rules with Regular Expressions

    main

    Tokens are defined using regular expressions. When defining rules, keep these behaviors in mind:

    • Order matters: Earlier rules take precedence over later ones.
    • Non-greedy quantifiers: Use *? instead of * to prevent tokens from consuming more text than intended (e.g., for strings).
    • Multiline behavior: The dot /./ does not match newlines. Use [^] if you need to match newlines.
    • Excluding characters: Be careful with /[^ ]/ or \s, as they may include newlines depending on your intent.
    // Use non-greedy quantifiers to avoid over-matching
    let lexer = moo.compile({
      string: /".*?"/,
    })
    
    lexer.reset('"foo" "bar"')
    lexer.next() // -> { type: 'string', value: 'foo' }
  3. Use lexer states for complex tokenization

    main

    Moo supports multiple states to handle context-sensitive tokenization (like string interpolation). Each state has its own set of rules. You can transition between states using annotations on rules:

    • next: 'stateName': Moves to the specified state without changing the stack.
    • push: 'stateName': Pushes the current state onto the stack and moves to the new state.
    • pop: 1: Removes the top state from the stack and returns to the previous state.

    Rules only match if they are defined within the current active state.

    let lexer = moo.states({
      main: {
        strstart: {match: '`', push: 'lit'},
        ident:    /\w+/,
      },
      lit: {
        strend:   {match: '`', pop: 1},
        const:    {match: /(?:[^$`]|\$(?!\{))+/, lineBreaks: true},
      },
    })
  4. Track line numbers and column positions

    main

    Moo tracks line and column numbers, but for performance reasons, this is disabled by default. To enable line tracking, you must apply the lineBreaks: true option to any rules that might contain newlines (such as a dedicated newline token).

    For optimal performance, match newlines in a dedicated token rather than inside every other rule.

    let lexer = moo.compile({
      newline: {match: '\n', lineBreaks: true},
      space:  {match: /[ \t]+/},
    })
  5. Install and use Moo

    main

    Moo is a highly-optimized tokenizer/lexer generator. You can install it via npm or use the standalone moo.js file in a web page.

    To use it, call moo.compile() with a configuration object defining your tokens using regular expressions or keyword lists. Once compiled, use lexer.reset(text) to load input and lexer.next() to retrieve tokens one by one. When lexer.next() returns undefined, the end of the buffer has been reached.

    const moo = require('moo')
    
    let lexer = moo.compile({
      WS:      /[ \t]+/,
      comment: /\/\/.*?$/,
      number:  /0|[1-9][0-9]*/,
      string:  /"(?:\\\\["\\]|[^\n"\\])*"/,
      lparen:  '(',
      rparen:  ')',
      keyword: ['while', 'if', 'else', 'moo', 'cows'],
      NL:      { match: /\n/, lineBreaks: true },
    })
    
    lexer.reset('while (10) cows\nmoo')
    lexer.next() // -> { type: 'keyword', value: 'while' }
    lexer.next() // -> { type: 'WS', value: ' ' }
    lexer.next() // -> { type: 'lparen', value: '(' }
    lexer.next() // -> { type: 'number', value: '10' }
  6. Transform token values

    main

    Moo does not support capturing groups in regular expressions. Instead, use the value property in your rule definition to provide a transformation function. This function receives the matched text and returns the processed value.

    const lexer = moo.compile({
      string: {match: /"(?:\\\\["\\]|[^\n"\\])*"/, value: s => s.slice(1, -1)},
    })
    
    lexer.reset('"test"')
    lexer.next() // -> { value: 'test', text: '"test"', ... }
  7. Iterate over tokens

    main

    Moo lexers are iterable. You can use a for...of loop to consume tokens or Array.from(lexer) to convert all tokens into an array. For lookahead capabilities, you can use the itt package.

    // Standard iteration
    for (let here of lexer) {
      // here is a Token object
    }
    
    // Convert to array
    let tokens = Array.from(lexer);
  8. Manage lexer state with reset() and save()

    main

    Calling reset(text) clears the internal buffer and resets line, column, and offset counts.

    To resume lexing from a specific point, use lexer.save() to capture the current state (including line/column info) and pass that object as the second argument to reset(text, info).

    lexer.reset('some line\n')
    let info = lexer.save() // -> { line: 10 }
    // ... consume tokens ...
    lexer.reset('a different line\n', info)
    lexer.next() // -> { line: 10 }
  9. Handle errors in Moo

    main

    By default, Moo throws an Error if no rules match. You can customize this behavior:

    1. Return an error token: Define a rule using moo.error. This token will contain the remainder of the buffer.
    2. Partial error matching: Define a rule that matches specific characters but is marked with error: true.
    3. Pretty-print errors: Use lexer.formatError(token, "message") to generate a human-readable error string showing the location of the offending token.
    // Return an error token instead of throwing
    moo.compile({
      myError: moo.error,
    })
    
    // Or match specific characters as errors
    moo.compile({
      myError: {match: /[\$?`]/, error: true},
    })
    
    // Format a pretty error message
    throw new Error(lexer.formatError(token, "invalid syntax"))
  10. Understand the Token object structure

    main

    The next() method returns a Token object with the following properties:

    • type: The name of the token group (from compile).
    • text: The raw string that was matched.
    • value: The string matched, transformed by a value function if provided.
    • offset: The byte offset from the start of the buffer.
    • lineBreaks: The number of line breaks found in the match (0 if lineBreaks: false).
    • line: The starting line number (starting from 1).
    • col: The starting column number (starting from 1).
  11. Iterate over tokens using the Lexer iterator

    main

    A Lexer instance is iterable. You can use a for...of loop to consume all tokens until EOF.

    const tokens = lexer; // Lexer implements Symbol.iterator
    for (const token of tokens) {
      console.log(token.type, token.text);
    }
  12. Compile a stateless lexer with `compile()`

    main

    Use compile(rules) to create a simple lexer that does not use states. The rules argument can be an object where keys are token types and values are patterns (strings or RegExps), or an array of rule objects.

    Rules can include:

    • match: A string or RegExp. If an array, they are tried in order.
    • type: A function to transform the token type based on the matched text.
    • value: A function to transform the token value based on the matched text.
    • lineBreaks: Boolean. If true, the lexer tracks line numbers for this token.
    • error: Boolean. If true, this rule acts as an error handler.
    • fallback: Boolean. If true, this rule acts as a fallback handler.
    • shouldThrow: Boolean. If true, the lexer throws an error when this rule matches.

    Note: In a stateless lexer, you cannot use push, pop, or next properties.

    const moo = require('moo');
    
    const lexer = moo.compile({
      whitespace: /\s*/,
      number: /\d+/,
      plus: '+',
      minus: '-',
      error: { match: /./, lineBreaks: true, shouldThrow: true }
    });
    
    const tokens = lexer.next();