Skip to content

[strawman]: declarative "template tools" #4153

@jonasfj

Description

@jonasfj

There is some debate about making first-class dartdoc directives for tools like snippets, and I was thinking why don't we have "template tools", or rather tools that don't involve running dart code, but maybe just evaluating a template.

This is an extremely ugly attempt at defining what that could look like, without us inventing a new language and still giving template authors some power.

Warning

Please don't think of this as a serious proposal, it's more of an exploratory idea 🤣

dartdoc:
  # What if we allowed "template tools"?
  template_tools:
    # We could almost define something like snippets that way.
    snippets:
      # Snippets don't have flags or options, so this is just illustrative
      flags:
        - warning     # ArgParser.addFlag('warning')      , allowing --[no]-warning (defaulting to false)
      options:
        - language    # ArgParser.addOption('language')   , allowing --language=foo

      # We have a context dictionary as follows:
      #   source.line
      #   source.column
      #   source.path
      #   package.name
      #   element.name
      #   invocationIndex
      #   input                         # text between {@tool snippets} and {@end-tool}
      #   arguments.raw                 # space separated arguments given in {@tool} invocation
      #   arguments.warning             # true, if --warning was given in {@tool} invocation
      #   arguments.language            # value, if --language=value was given in {@tool} invocation
      #
      # Using this context we can do some limited steps like:
      #  - extract variables from context using regular expressions and save them to context again.
      #  - assert values from context and throw an error instead of rendering a faulty template!
      steps:
      # Create a new "description" field in the context dictionary, running pattern as regex against text
      - extract: description
        text:           '{{input}}'
        pattern:        '^(?<content>.*)^```'
        multiLine:      true
        caseSensitive:  true
        dotAll:         true
      - extract: code
        text:           '{{input}}'
        pattern:        '^```(?<language>.*)\n(?<source>.*)^```'
        multiLine:      true
        caseSensitive:  true
        dotAll:         true
      # Evaluate assertion with package:boolean_selector (already used in package:test)
      # Giving template tool authors an option to sanity check data before the template is rendered.
      - assert: 'code.hasMatch && description.hasMatch'
        message: 'Failed to extract code and language from snippet'

      # After running "steps" we've expanded context with:
      #   description.hasMatch  # True, if regex was matched
      #   description.content   # capture group content
      #   code.hasMatch         # True, if regex was matched!
      #   code.language         # capture group language
      #   code.source           # capture group source
      #
      # We can access context in the template using mustache as illustrated below:
      template: |
        {@inject-html}
        <a name="{{element.name}}.{{invocationIndex}}"></a>
        <div class="snippet snippet-container anchor-container">
          {{description}}
          <a class="anchor-button-overlay anchor-button" title="Copy link to clipboard"
            onmouseenter="fixHref(this, '{{element.name}}.{{invocationIndex}}');" onclick="fixHref(this, '{{element.name}}.{{invocationIndex}}'); copyStringToClipboard(this.href);"
            href="#">
            <i class="material-icons anchor-image">link</i>
          </a>
          <div class="copyable-container">
            <button class="copy-button-overlay copy-button" title="Copy to clipboard"
              onclick="copyTextToClipboard(findSiblingWithId(this, 'sample-code'));">
              <i class="material-icons copy-image">content_copy</i>
            </button>
            <pre class="language-{{code.language}}" id="sample-code"><code class="language-{{code.language}}">{{code.source}}</code></pre>
          </div>
        </div>
        {@end-inject-html}

Note that context can probably just be Map<String, String>, we dont' have to treat foo.bar as sub-property access :D

Note:

  • You can still do some attacks by using degenerate regular expressions
    • We could probably accept that since it'll just loop indefinitely (or be slow), or,
    • We could evaluate the regular expressions in an isolate (which is cheap).
  • Regexes gives template tool authors a lot of power to do at-least something.
  • The steps of repeated regex evaluation is a powerful, but also horrific programming environment 🤣

I'm not sure this is the way, we could also implement a small expression language that can be safely evaluated in templates. Options range form jinja-like things, to implementing a safe subset Dart in a small interpreter. To porting json-e to Dart and feeding the output into mustache.

But we could probably get far with regexes, boolean_selector for assertions and mustache for templating.


All this being explored: We should perhaps ask the question if we want people inventing their own HTML section types. Or whether it'd be better to argue that:

  • (A) users should be using existing markdown constructs (there aren't much missing).
  • (B) We should introduce dartdoc directives when good markdown constructs are missing.

In the case of snippets, it could be argued that:

  • Using a ## Section title in markdown is better than a snippet macro.
  • All sections in dartdoc markdown should have a perma-link.
  • All code blocks should have a "copy to clipboard" button.

Though, we'd have to give up the special background color for snippet section blocks.

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-enhancementA request for a change that isn't a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions