Skip to content

Templates ​

Filenames, file contents, and the commit message are all Nunjucks templates rendered once per submission. commit.prefix is the one exception: it is a literal marker and is not templated.

Variables ​

submission ​

VariableDescription
submission.idLeetCode submission id (number)
submission.timestampUnix seconds
submission.dateYYYY-MM-DD (UTC)
submission.langLanguage id (python3, typescript, …)
submission.lang_extFile extension mapped from the language (py, ts, …)
submission.codeThe solution code
submission.statuse.g. Accepted
submission.runtimee.g. 42 ms (null when unavailable)
submission.memorye.g. 13.5 MB (null when unavailable)
submission.runtime_percentileNumber (null when unavailable)
submission.memory_percentileNumber (null when unavailable)

question ​

VariableDescription
question.frontend_ide.g. 1
question.titlee.g. Two Sum
question.title_sluge.g. two-sum
question.difficultyEasy | Medium | Hard
question.tagsArray of topic tag names
question.urlhttps://leetcode.com/problems/<slug>/
question.contentRaw problem HTML (convert with toMarkdown / toTypst)
question.acceptance_rateNumber (null when unavailable)
question.is_paid_onlyBoolean

Undefined variables render empty by default; set render.throwOnUndefined: true to make them throw instead — useful for catching typos.

Built-in filters ​

Every standard Nunjucks filter works: join, lower, replace, trim, truncate, capitalize, striptags, sort, groupby, dump, urlencode, and so on. leech adds a set of its own — see Filters & conversion.

A worked example ​

This produces a frontmatter-rich README, a Typst document, and the raw code:

yaml
files:
  - filename: "{{ question.title_slug }}/README.md"
    content: |
      ---
      title: "{{ question.title }}"
      id: {{ question.frontend_id }}
      difficulty: {{ question.difficulty }}
      date: "{{ submission.timestamp | datefmt('YYYY-MM-DD') }}"
      tags: [{% for t in question.tags %}"{{ t }}"{% if not loop.last %}, {% endif %}{% endfor %}]
      ---

      # {{ question.title }}

      {{ question.content | toMarkdown({ gfm: true }) }}

  - filename: "{{ question.title_slug }}.typ"
    content: |
      = {{ question.title }}

      {{ question.content | toTypst }}

      == Submission

      {{ submission.code | codeBlock(submission.lang) }}

  - filename: "{{ question.title_slug }}/{{ submission.lang }}.{{ submission.lang_ext }}"
    content: "{{ submission.code }}"

Filename sanitization ​

Rendered filenames are sanitized per path segment. Reserved characters (/\?%*:|"<> and control characters) and whitespace become -; runs of dashes collapse; leading and trailing dots and dashes are stripped; empty segments become untitled. This means a problem title with a slash or colon can't escape its directory or break on Windows checkouts.

Collisions and overwrites ​

Filenames are not unique by construction. If two submissions render the same path (for example, the same problem solved twice in the same language, or a README.md per problem), the later commit overwrites the earlier file. That's usually what you want — an improved solution replaces the old one — but if you need history preserved, include a unique key in the filename:

yaml
- filename: "{{ question.title_slug }}/{{ submission.id }}/{{ submission.lang }}.{{ submission.lang_ext }}"
  content: "{{ submission.code }}"

Pitfall: no nested braces inside filter arguments ​

A filter argument is a plain expression, so reference variables directly:

jinja
{{ submission.code | codeBlock(submission.lang) }}

Writing codeBlock({{ submission.lang }}) is a Nunjucks syntax error. It looks for a dict literal and fails with parseAggregate: expected colon after dict key, reported against the template's line and column — which is easy to mistake for a YAML error in the workflow file.

Released under the GLWTPL — absolutely no warranty. Good luck.