Week 19 of 2026
Development log of Devlog Excavator
7 items
- Make weekly entries, group them by project
- Add an author to the signature of each commit
- Let the excavator write stats for each entry
- Use correct ISO8601 week number
- Add more "stats" to the markdown documents
- In front-matter config rename stats to metadata
- Replace the dry run scenario with a real one
Make weekly entries, group them by project
On by
I've realized that what I want is for each project to have it's devlog in a separate directory. Also devlog entries should span a week, not a day. So the new structure will be like this:
project-alpha/2026-W01.md
The document will contain all the commits from week 1 of 2026.
Evaluating spec based on Nushell output was difficult and flaky. I want to rewrite the assetions in spec to introspect the directory structure and markdown output. This should be easier, since recent addition of Markdown parser to Nushell. But the spec is currently outdated. Evil me went ahead with the implementation without rewriting it.
Internally, now all the data is collected, processed and prepared to be
written out. With the --dry-run option, all this data is returned,
presumably for further processing, because it's too much to eyeball.
In a real run, the markdown files are written and only short stats are returned.
As I was working on this, I refactored and cleaned up some code.
index 82e781f..1924ff3 100755
--- a/excavate
+++ b/excavate
@@ -78,6 +78,29 @@ export def main [
= }
= }
=
+ ## Set file path and markdown content to write
+ let prepare_to_write = { |entry|
+
+ $entry
+ | get project
+ | str kebab-case
+ | { parent: $out_dir, stem: $in }
+ | path join
+ | { parent: $in, stem: $entry.week, extension: "md" }
+ | path join
+ | let out_path
+
+ let title = $"($entry.project) ($entry.week)"
+
+ $entry
+ | format entry $title $config
+ | let markdown
+
+ $entry
+ | insert path $out_path
+ | insert markdown $markdown
+ }
+
= # The main pipeline:
= $projects_dir
= | project list --ignore $config.ignore.paths
@@ -88,18 +111,19 @@ export def main [
= | insert project $commit_project
= | move day project author email subject time sha path --first
= | let commits
- | group-by --to-table day project
- | sort-by day
+ | insert week { |row| $row.day | format date "%Y-W%W" }
+ | group-by project week --prune --to-table
+ | sort-by week
+ | each $prepare_to_write
= | let entries
=
= let stats = {
= commits: ($commits | length),
= projects: ($entries | get project | uniq | length),
- days: ($entries | get day | uniq | length)
+ weeks: ($entries | get week | uniq | length)
+ entries: ($entries | length)
= }
=
- log info $"Excavated ($stats.commits) commits from ($stats.projects) projects across ($stats.days) days"
-
= # Warn about unlisted projects
= $commits
= | where { is-empty project }
@@ -108,30 +132,23 @@ export def main [
= | uniq-by path
= | each { log warning $"Activity in an unlisted project on ($in.day): ($in.path)" }
=
+ log debug $"Excavated ($stats.commits) commits from ($stats.projects) projects across ($stats.weeks) weeks"
+
= if $dry_run {
- $commits
- # If not running as a Nushell command, format time in the output
- | if running_as_a_script in $env { update time { format date } } else { $in }
- | return $in
- }
-
- # TODO: Allow setting in a config file
- mkdir $out_dir
+ # Return all the collected data, maybe for further processing
+ return $entries
+ } else {
+ for entry in $entries {
+ log info $"Writing to ($entry.path) …"
=
+ $entry.path
+ | path dirname
+ | mkdir $in
=
- for entry in $entries {
- $entry
- | get day project
- | str join "-"
- | str kebab-case
- | { parent: $out_dir, stem: $in, extension: "md" }
- | path join
- | let out_path
-
- log info $"Writing to ($out_path)..."
- $entry
- | format entry $in.project $config
- | save --force $out_path
+ $entry.markdown
+ | save --force $entry.path
+ }
+ return $stats
= }
=}
=
@@ -245,31 +262,33 @@ def "format entry" [
= title: string
= config: record
=] {
- let substitute = { |entry|
+ let apply_substitutions = { |text|
= $config.substitute
- | reduce --fold $entry { |substitute, memo|
+ | reduce --fold $text { |substitute, memo|
= $memo | str replace --all $substitute.what $substitute.with
= }
= }
=
+ let entry = $in
+
+ # # TODO: Write this in front-matter (extra.stats.commits or something)
+ # let stats = { commits: ($in.items | length) }
+
= let frontmatter = { title: $title }
- | insert $config.frontmatter.projects_cell_path [ $title ]
+ # TODO: | insert $config.frontmatter.stats_cell_path [ $stats ]
=
= [
= $"---"
= ($frontmatter | to yaml)
= $"---"
= $""
- # TODO: Write this in front-matter (extra.stats.commits or something)
- $"Commits: ($in.items | length)"
- $""
= $""
= (
- $in.items
+ $entry.items
= | each { | commit | $commit | format commit }
- | each $substitute
+ | each $apply_substitutions
= | str join "\n\n"
- )
+ )
= ] | str join "\n"
=}
=
@@ -289,9 +308,11 @@ def "format commit" [] {
=
= [
= $"# ($in.subject | str trim)"
- ""
+ $""
+ $"<time datetime=\"($in.day)\">($in.day | format date '%A, %F')</time>"
+ $""
= ($body | str trim)
- ""
+ $""
= ($patch | each { format diff } | str join "\n\n" )
= ] | str join "\n"
=}Add an author to the signature of each commit
On by
After the time, on the same line.
index 1924ff3..ab4e743 100755
--- a/excavate
+++ b/excavate
@@ -305,16 +305,24 @@ def "format commit" [] {
= | split row --regex "(?m)^diff --git .+" | skip 1
= }
= | let patch
-
+
+ [
+ $"On <time datetime=\"($in.day)\">($in.day | format date '%A, %F')</time>"
+ $"by <span class=\"author\">($in.author)</span>"
+ ]
+ | str join " "
+ | let signature
+
= [
= $"# ($in.subject | str trim)"
= $""
- $"<time datetime=\"($in.day)\">($in.day | format date '%A, %F')</time>"
+ $signature
= $""
= ($body | str trim)
= $""
= ($patch | each { format diff } | str join "\n\n" )
- ] | str join "\n"
+ ]
+ | str join "\n"
=}
=
=def "format diff" [] {Let the excavator write stats for each entry
On by
In the front-matter, under a configurable cell-path.
index bcc83b7..a75c053 100644
--- a/devlog-sample.toml
+++ b/devlog-sample.toml
@@ -3,10 +3,9 @@ projects_stubs_dir = "content/works/"
=
=[frontmatter]
=
-# By default projects are listed under extra.projects. This example shows how
-# to use custom cell path to list them under taxonomies, for example to use with
-# Zola (https://www.getzola.org/documentation/content/taxonomies/).
-projects_cell_path = [ "taxonomies", "projects" ]
+# By default stats are listed in front-matter under <extra>. This example shows how
+# to use custom cell path to list them under <extra.stats>.
+stats_cell_path = [ "extra", "stats" ]
=
=# Each project you want to excavate should have it's own section, like this:
=[[projects]]index ab4e743..233c18a 100755
--- a/excavate
+++ b/excavate
@@ -271,11 +271,19 @@ def "format entry" [
=
= let entry = $in
=
- # # TODO: Write this in front-matter (extra.stats.commits or something)
- # let stats = { commits: ($in.items | length) }
+ let stats = {
+ commits: ($entry.items | length)
+ authors: ($entry.items | get author | uniq )
+ }
=
= let frontmatter = { title: $title }
- # TODO: | insert $config.frontmatter.stats_cell_path [ $stats ]
+ | upsert $config.frontmatter.stats_cell_path { |row|
+ $row
+ | get --optional $config.frontmatter.stats_cell_path
+ | default {}
+ | merge $stats
+ }
+
=
= [
= $"---"
@@ -338,7 +346,7 @@ def "format diff" [] {
=def "normalize config" []: record -> record {
= $in
= | update projects { update path { path expand } } # Resolve all project paths
- | upsert frontmatter.projects_cell_path { default { [ extra projects ] } | into cell-path }
+ | upsert frontmatter.stats_cell_path { default { [ extra ] } | into cell-path }
= | upsert ignore.paths { default [] | each { path expand } }
= | upsert projects_dir { default "~/Projects" | path expand }
= | upsert projects_stubs_dir { default "./drafts/projects" | path expand }Use correct ISO8601 week number
On by
I.e. with the first week of the year denoted as 01, instead of 00.
index 233c18a..94d865e 100755
--- a/excavate
+++ b/excavate
@@ -111,7 +111,7 @@ export def main [
= | insert project $commit_project
= | move day project author email subject time sha path --first
= | let commits
- | insert week { |row| $row.day | format date "%Y-W%W" }
+ | insert week { |row| $row.day | format date "%Y-W%V" }
= | group-by project week --prune --to-table
= | sort-by week
= | each $prepare_to_writeAdd more "stats" to the markdown documents
On by
It's more like metadata really. Anyway, I use those fields on tad-lispy.com, so I need them.
index 94d865e..2f04892 100755
--- a/excavate
+++ b/excavate
@@ -271,9 +271,19 @@ def "format entry" [
=
= let entry = $in
=
+ $entry.items
+ | get day
+ | sort
+ | last
+ | let last_day
+
= let stats = {
= commits: ($entry.items | length)
= authors: ($entry.items | get author | uniq )
+ year: ($last_day | format date %Y)
+ week: ($last_day | format date %V)
+ iso_week: ($entry.week)
+ last_day: ($entry.items | get day | sort | last)
= }
=
= let frontmatter = { title: $title }In front-matter config rename stats to metadata
On by
It's a better name. Only number of commits is a statistic. The rest of the fields are various metadata.
It would also be nice to set cell-path separately for different fields.
For example in Zola there is a top-level authors field, that could be
populated with names of commits' authors.
index a75c053..422ece2 100644
--- a/devlog-sample.toml
+++ b/devlog-sample.toml
@@ -5,7 +5,7 @@ projects_stubs_dir = "content/works/"
=
=# By default stats are listed in front-matter under <extra>. This example shows how
=# to use custom cell path to list them under <extra.stats>.
-stats_cell_path = [ "extra", "stats" ]
+metadata_cell_path = [ "extra", "stats" ]
=
=# Each project you want to excavate should have it's own section, like this:
=[[projects]]index 2f04892..ad735fe 100755
--- a/excavate
+++ b/excavate
@@ -277,7 +277,7 @@ def "format entry" [
= | last
= | let last_day
=
- let stats = {
+ let metadata = {
= commits: ($entry.items | length)
= authors: ($entry.items | get author | uniq )
= year: ($last_day | format date %Y)
@@ -287,11 +287,11 @@ def "format entry" [
= }
=
= let frontmatter = { title: $title }
- | upsert $config.frontmatter.stats_cell_path { |row|
+ | upsert $config.frontmatter.metadata_cell_path { |row|
= $row
- | get --optional $config.frontmatter.stats_cell_path
+ | get --optional $config.frontmatter.metadata_cell_path
= | default {}
- | merge $stats
+ | merge $metadata
= }
=
=
@@ -356,7 +356,7 @@ def "format diff" [] {
=def "normalize config" []: record -> record {
= $in
= | update projects { update path { path expand } } # Resolve all project paths
- | upsert frontmatter.stats_cell_path { default { [ extra ] } | into cell-path }
+ | upsert frontmatter.metadata_cell_path { default { [ extra ] } | into cell-path }
= | upsert ignore.paths { default [] | each { path expand } }
= | upsert projects_dir { default "~/Projects" | path expand }
= | upsert projects_stubs_dir { default "./drafts/projects" | path expand }Replace the dry run scenario with a real one
On by
The assertions are only tested about one out of four documents. It requires a lot of steps. I need to figure out a way to write it more concisely before testing the other three.
Two improvements were discovered and applied while working on it.
- The last year and week in metadata are now integers, no strings
- The files written in fictional projects end with newlines
The tests are quite comprehensive, checking the emitted document element by element.
index ad735fe..b5f4c6d 100755
--- a/excavate
+++ b/excavate
@@ -280,8 +280,8 @@ def "format entry" [
= let metadata = {
= commits: ($entry.items | length)
= authors: ($entry.items | get author | uniq )
- year: ($last_day | format date %Y)
- week: ($last_day | format date %V)
+ year: ($last_day | format date %Y | into int)
+ week: ($last_day | format date %V | into int)
= iso_week: ($entry.week)
= last_day: ($entry.items | get day | sort | last)
= }index 888f0b5..a12aeb5 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -7,14 +7,15 @@ interpreter: nu --stdin spec/interpreters/basic.nu
=This program excavates a devlog (i.e. a bunch of .md files) from commit messages in multiple repositories.
=
=
-## Listing Commits to Excavate
+## Basic Usage
=
=``` yaml tbb
=tags: [ focus ]
=```
=
-Before you excavate, it might be smart to see what the result would be. You can use `--dry-run` flag for that. Consider the following scenario.
+Consider a team of three developers (Alice, Bob, and Charlie) working on two projects (Alpha and Beta). Here's how they can use Devlog Excavator to extract Markdown documents from their thoughtful commit messages.
=
+Steps:
=
= * Develop project `Alpha`
= * Develop project `Beta`
@@ -25,43 +26,159 @@ Before you excavate, it might be smart to see what the result would be. You can
= name = "Project Alpha"
= path = "./alpha"
=
- # [[projects]]
- # name = "Project Beta"
- # path = "./project-beta"
+ [[projects]]
+ name = "Project Beta"
+ path = "./beta"
= ```
=
- * Run the `excavate --projects-dir . --dry-run` command
+ * Run the `excavate --projects-dir .` command
=
- TODO: Use structured data for assertions. Currently the tests are very flaky wrt terminal width.
-
- * Expect the output to contain `the header row`
+ * Expect the output to contain `the stats row`
=
- It contains column names, as produced by Nushell. The output format should be configurable.
+ The standard output should be a single Nushell formatted table with a general statistics of the completed operation.
=
- ``` regexp
- │ +# +│ ++day +│ +project +│ +author +│ +email +│ +subject +│ +time +│ +sha +│ +path +│
+ ``` text
+ ╭──────────┬────╮
+ │ commits │ 10 │
+ │ projects │ 2 │
+ │ weeks │ 2 │
+ │ entries │ 4 │
+ ╰──────────┴────╯
+ ```
+
+ * Change directory to `drafts/devlog`
+
+ * There are only the following documents
+
+ 1. project-alpha/2026-W13.md
+ 2. project-alpha/2026-W14.md
+ 3. project-beta/2026-W13.md
+ 4. project-beta/2026-W14.md
+
+ * Open `project-alpha/2026-W13.md`
+
+ * The title is `Project Alpha 2026-W13`
+
+ * The front-matter has an `extra` field with the following `yaml` data
+
+ ``` nushell
+ open project-alpha/2026-W13.md
+ | get 0
+ | get attrs.value
+ | from yaml
+ | get extra
+ | to yaml
= ```
=
- * Expect the output to contain `the first commit from Alice`
-
- ``` regex
- │ +0 +│ +2026-03-27 +│ +Project Alpha +│ +Alice +│ +alice@example.com +│ +Write a readme +│ +Fri, 27 Mar 2026 13:22:00 \+\d{4} +│ +[0-9a-f]+ +│ +.+/alpha +│
+ ``` yaml
+ commits: 3
+ authors:
+ - Alice
+ - Bob
+ year: 2026
+ week: 13
+ iso_week: 2026-W13
+ last_day: 2026-03-27
= ```
=
- > NOTE: To make the spec portable, the timezone, sha and part of the path has to be expressed as wildcard.
+ * The first element has type `h1` and text `Write a readme`
=
- * Expect the output to contain `the late night commit from Alice`
+ That's the output from the first commit from Alice
=
- Notice that the logical day (in the second column) is one before the actual day (in the seventh column). That's because work done at night is still counted toward the previous day. This can be controlled with `start-of-day` configuration parameter.
+ ``` nushell
+ open project-alpha/2026-W13.md
+ | where type == h1
+ | where children.0.attrs.value == "Write a readme"
+ | get 0.position.end.line
+ ```
+
+ * It's immediately followed by an element of type `text` with text `On `
+ * It's immediately followed by an element of type `html` with text `<time datetime="2026-03-27">`
+ * It's immediately followed by an element of type `text` with text `Friday, 2026-03-27`
+ * It's immediately followed by an element of type `html` with text `</time>`
+ * It's immediately followed by an element of type `text` with text ` by `
+
+ There are two spaces on either side of the word. One will be removed. See https://spec.commonmark.org/0.31.2/#code-spans
=
- ``` regex
- │ +2 +│ +2026-03-27 +│ +Project Alpha +│ +Alice +│ +alice@example.com +│ +Make Project Alpha work +│ +Sat, 28 Mar 2026 03:06:00 \+\d{4} +│ +[0-9a-f]+ +│ +.+/alpha +│
+ * It's immediately followed by an element of type `html` with text `<span class="author">`
+ * It's immediately followed by an element of type `text` with text `Alice`
+ * It's immediately followed by an element of type `html` with text `</span>`
+ * It's immediately followed by an element of type `text` with text `It's very exciting to start a new project.`
+
+ * It's immediately followed by this block of `diff` code
+
+ ``` diff
+ new file mode 100644
+ index 0000000..459bb5f
+ --- /dev/null
+ +++ b/README.md
+ @@ -0,0 +1,3 @@
+ +# This is Project Alpha
+ +
+ +The best project ever. Like seriosuly.
+ ```
+
+ * It's immediately followed by an element of type `h1` with text `Implement an MVP`
+ * It's immediately followed by an element of type `text` with text `On `
+ * It's immediately followed by an element of type `html` with text `<time datetime="2026-03-27">`
+ * It's immediately followed by an element of type `text` with text `Friday, 2026-03-27`
+ * It's immediately followed by an element of type `html` with text `</time>`
+ * It's immediately followed by an element of type `text` with text ` by `
+ * It's immediately followed by an element of type `html` with text `<span class="author">`
+ * It's immediately followed by an element of type `text` with text `Bob`
+ * It's immediately followed by an element of type `html` with text `</span>`
+ * It's immediately followed by an element of type `text` with text `I need to keep my phone charged. Investors are going to call any minute now.`
+ * It's immediately followed by this block of `diff` code
+
+ ```diff
+ new file mode 100644
+ index 0000000..22249c0
+ --- /dev/null
+ +++ b/src/main.rs
+ @@ -0,0 +1,3 @@
+ +fn main() {
+ + println ("Hello, World!")
+ +}
+ ```
+
+ * It's immediately followed by an element of type `h1` with text `Make Project Alpha work`
+ * It's immediately followed by an element of type `text` with text `On `
+ * It's immediately followed by an element of type `html` with text `<time datetime="2026-03-27">`
+ * It's immediately followed by an element of type `text` with text `Friday, 2026-03-27`
+ * It's immediately followed by an element of type `html` with text `</time>`
+ * It's immediately followed by an element of type `text` with text ` by `
+ * It's immediately followed by an element of type `html` with text `<span class="author">`
+ * It's immediately followed by an element of type `text` with text `Alice`
+ * It's immediately followed by an element of type `html` with text `</span>`
+ * It's immediately followed by an element of type `text` with text `Coding is hard for Bob. He should have used Tad Better Behavior to test his stuff before pushing it to prod!`
+ * It's immediately followed by this block of `diff` code
+
+ ```diff
+ new file mode 100644
+ index 0000000..a17027d
+ --- /dev/null
+ +++ b/CHANGELOG.md
+ @@ -0,0 +1,3 @@
+ +# v1.0.0
+ +
+ +* Now it's working
= ```
=
- * Expect the output to contain `the silly commit from Bob`
+ * It's immediately followed by this block of `diff` code
+
+ ```diff
+ index 22249c0..891ede1 100644
+ --- a/src/main.rs
+ +++ b/src/main.rs
+ @@ -1,3 +1,3 @@
+ =fn main() {
+ - println ("Hello, World!")
+ + println!("Hello, World!")
+ =}
+ ```
+
+ * There is no more content in the document
=
- ``` regex
- │ +3 +│ +2026-04-01 +│ +Project Alpha +│ +Bob +│ +bob@example.com +│ +I've lawyered up +│ +Wed, 1 Apr 2026 09:03:00 \+\d{4} +│ +[0-9a-f]+ +│ +.+/alpha +│
= ```
=
=## The reportindex 49cb70b..978136b 100644
--- a/spec/interpreters/basic.nu
+++ b/spec/interpreters/basic.nu
@@ -1,4 +1,5 @@
=use tbb.nu
+use std/assert
=
=def write [
= --force # Overwite existing files?
@@ -69,6 +70,7 @@ def main [] {
= $data.code_blocks
= | first
= | get value
+ | $in + "\n" # Ensure there is a newline at the end of the code
= | write $path
=
= },
@@ -81,6 +83,7 @@ def main [] {
= $data.code_blocks
= | first
= | get value
+ | $in + "\n" # Ensure there is a newline at the end of the code
= | write --force $path
= },
=
@@ -141,8 +144,174 @@ def main [] {
= $data.state.output
= | ansi strip
= | tbb observe snippet --caption "The haystack"
- | tbb assert match $pattern
+ | let haystack
+
+ if ($data.code_blocks | first | get language) == "regex" {
+ $haystack | tbb assert match $pattern
+ } else {
+ $haystack | str contains $pattern
+ }
+ },
+
+ "There are only the following documents": { |data|
+ cd $data.state.pwd
+
+ $data.lists
+ | each { get items.value }
+ | flatten
+ | let expected
+ | tbb observe snippet --caption "Expected documents"
+
+ ls **/*
+ | where type == file
+ | get name
+ | let existing
+ | tbb observe snippet --caption "Existing documents"
+ | where { not ($in in $expected) }
+ | let unexpected
+
+ $expected
+ | where { not ($in in $existing) }
+ | let missing
+
+ if ($missing | is-not-empty) {
+ error make {
+ msg: "Expected documents not found",
+ help: ($missing | to md)
+ }
+ }
+
+ if ($unexpected | is-not-empty) {
+ error make {
+ msg: "Unexpected documents found",
+ help: ($unexpected | to md)
+ }
+ }
+ }
+
+
+ "Open {0}": { |path, data|
+ $data.state.pwd
+ | path join $path
+ | let absolute_path
+ | tbb observe link $"Opening ($path)"
+ | open $in
+ | { document: $in, document_path: $absolute_path }
+ }
+
+ "The title is {0}": { |expected, data|
+ $data.state.document.0.attrs.value
+ | tbb observe snippet --caption $"Frontmatter of ($data.state.document_path)" --raw "yaml"
+ | from yaml
+ | get title
+ | assert equal $in $expected
+ }
+
+ "The front-matter has an {0} field with the following {1} data": { |cell_path, format, data|
+ $data.code_blocks
+ | where language == $format
+ | get 0.value
+ | do {
+ let raw = $in
+ match $format {
+ "yaml" | "yml" => ($raw | from yaml)
+ "json" => ($raw | from json)
+ "toml" => ($raw | from toml)
+ "csv" => ($raw | from csv)
+ "nuon" => ($raw | from nuon)
+ "ssv" => ($raw | from ssv)
+ "tsv" => ($raw | from tsv)
+ "xml" => ($raw | from xml)
+
+ _ => (error make {msg: $"Unsupported format: ($format)"})
+ }
+ }
+ | let expected
+ | tbb observe snippet --caption $"Value expected at ($cell_path)" --transform { describe --detailed }
+
+ # $cell_path
+ # | into cell-path
+ # | let cell_path
+
+ $data.state.document.0.attrs.value
+ | from yaml
+ | get $cell_path
+ | tbb observe snippet --caption $"Actual value at ($cell_path)" --transform { describe --detailed }
+ | assert equal $in $expected
+ }
+
+ "The first element has type {0} and text {1}": { |expected_type, expected_text, data|
+ $data.state.document.1
+ | let element
+
+ assert equal $element.type $expected_type
+
+ $element
+ | inner-text
+ | assert equal $in $expected_text
+
+ { looking_at: 1 }
+ },
+
+ "It's immediately followed by an element of type {0} with text {1}": { |expected_type, expected_text, data|
+ $data.state.looking_at
+ | $in + 1
+ | let looking_at
+
+ $data.state.document
+ | get $looking_at
+ | let element
+
+ assert equal $element.type $expected_type
+
+ $expected_text
+ | describe --detailed
+ | to json
+ | tbb observe snippet --caption "Expected text" --raw "json"
+
+ $element
+ | inner-text
+ | assert equal $in $expected_text
+
+ { looking_at: $looking_at }
+ },
+ "It's immediately followed by this block of {0} code": { |language, data|
+ $data.code_blocks
+ | where language == $language
+ | first
+ | get value
+ | let expected_code
+
+ $data.state.looking_at + 1
+ | let looking_at
+
+ $data.state.document
+ | get $looking_at
+ | let element
+
+ assert equal $element.type "code"
+ assert equal $element.attrs.lang $language
+ assert equal $element.attrs.value $expected_code
+
+ { looking_at: $looking_at }
+ },
+
+ "There is no more content in the document": { |data|
+ assert equal ($data.state.document | length) ($data.state.looking_at + 1)
= }
= }
=}
=
+def inner-text []: record -> string {
+ $in
+ | get --optional attrs.value
+ | default ""
+ | let own_text
+
+ $in.children
+ | each { inner-text }
+ | str join ""
+ | let children_text
+
+ $own_text + $children_text
+}