Generated Content Tables

See also: Linking Between Content Notes, Shortcode Integrity, System Development

A catalog table — every cloth armour, every animal’s attributes, every trauma of a category — is data that already lives in the frontmatter of the notes it describes. Authoring such a table by hand duplicates that data and guarantees drift: the item’s weight changes, the table does not, and nothing in the build notices.

A content body therefore declares what it wants tabulated and the build fills in the rows. The declaration is an ordinary Obsidian Dataview TABLE query, in a fenced dataview block:

```dataview
TABLE WITHOUT ID
  link(file.path, name.full) AS "Name",
  sohl.weight AS "Weight",
  sohl.protection.blunt AS "B"
WHERE type = "armorgear" and sohl.material = "Cloth"
SORT name.full ASC
```

Content is authored in assets/content/, which is opened as an Obsidian vault so the Dataview plugin renders that block live. The build renders the same query against the same frontmatter, so one authored query yields the same table in three places: in Obsidian while writing, in the Foundry compendium packs, and on the knowledgebase. What the author sees is what ships.

Both content builds run the same expander (@heroiclands/package-build/engine/content-tables).

Supported grammar

Only the subset below is implemented. Anything outside it is a build error that names the offending clause — never a silently wrong table.

TABLE [WITHOUT ID] <column> [, <column>]*
[FROM <source>]
[WHERE <expression>]
[SORT <key> [ASC|DESC] [, …]]
[LIMIT <n>]

Keywords are case-insensitive (table/TABLE, as/AS, and/AND). Clauses must appear in the order above, once each — which is what lets a frontmatter field share a clause keyword’s name, as the traits table’s SORT sort ASC does.

ClauseMeaning
TABLEThe columns. WITHOUT ID drops Dataview’s implicit leading File column.
FROMRestrict to a folder (FROM "Creatures") or a tag (FROM #animal).
WHEREKeep the notes the expression holds for.
SORTSort keys, each optionally ASC (default) or DESC.
LIMITKeep only the first n rows, after sorting.

LIST, TASK, and CALENDAR queries, and the GROUP BY and FLATTEN commands, are refused by name.

Columns

A column is an expression, optionally named with AS "Header"; without AS, the expression’s own text is the header. Values render as follows:

  • absent or empty → an em dash ();
  • an array → its elements, comma-separated;
  • a boolean → yes / no;
  • an object → a build error. An expression resolving to an object is almost always a truncated path (sohl.protection for sohl.protection.blunt), and would otherwise ship as [object Object].

A column whose every shown value is numeric is right-aligned; | and newlines in a value are escaped so a cell cannot break out of the table.

Linking a row to its note

link(file.path, name.full) is emitted as a [[type/shortcode|Name]] wikilink to the row’s own note, which each build then resolves the way it resolves any other wikilink: into a @UUID enricher for Foundry, and into a site href for the knowledgebase. The same query therefore yields a clickable catalog in all three places. The implicit File column (a TABLE written without WITHOUT ID) links the same way.

A note the build cannot address that way — one carrying no type or no shortcode — renders as plain text rather than shipping a literal [[…]] into a journal.

Fields

Any frontmatter property is addressable, in either the columns or the WHERE clause, as a dotted path (sohl.protection.blunt) or a bracketed key (sohl["subType"], needed when a key is not a bare word). A path that names nothing is null, not an error.

file.* names the note’s place in the tree instead:

FieldValue
file.pathLocation below assets/content/Creatures/Animal/Aurochs.md
file.folderIts directory — Creatures/Animal
file.nameIts filename without the extension — Aurochs
file.linkA link to the note itself
file.tagsIts tags, each with a leading #, plus every parent of a nested tag
file.etagsIts tags exactly as written, without parent expansion

An unknown file.* field is a build error — it would otherwise read as a table that silently matches nothing.

this is the note containing the query (not the row), so this.package or this["name.full"] reads the page the table is written on.

Expressions

FormMeaning
a and b, a or b, not a, !a, ( … )Boolean combination
type = "being"Equality — case-sensitive, as Dataview’s is
intensity != "attribute"Inequality
sohl.value > 90, >=, <, <=Ordering; numeric when both sides are numbers
shortcodeA bare field is a presence test (absent/empty/zero is false)
!shortcodeAbsence
"text", 12, true, null, [a, b]Literals

Functions: contains / icontains / econtains, startswith, endswith, lower, upper, length, default, number, string, join, regexmatch, regextest, and link. An unknown function is a build error.

contains() recurses into a list and substring-matches a string — which is why contains(file.tags, "cooking") matches the tag #cooking. It is case-sensitive; icontains() is the forgiving variant and econtains() demands an exact element.

Sorting

Rows are ordered by the SORT keys — numerically where both values are numbers, otherwise as text and case-insensitively, with empty values last. Ties break on the note’s content path and then its id, so a table emits identically on every build. With no SORT clause, rows keep content-path order.

Note the asymmetry, which Dataview shares: = is case-sensitive, but ordering is not. "Horn, Hunting" belongs beside "Horn, fanfare", not before it.

Scope and failure

A table searches only notes of the source note’s own package, so a SoHL page never tabulates setting-package content, and vice versa.

A query that cannot be honoured — malformed, naming an unsupported clause, or calling an unknown function — is a build error, and the block is left in the body verbatim so the failure is visible in the output as well as on the console. In the pack build the note fails to compile; in the knowledgebase build the run exits non-zero.

A query that matches no note is not an error: it renders as an empty table (headers only), which is exactly what the author already sees in Obsidian. A category with no content written yet is a normal state of the corpus, not a broken build.

Where it runs

Expansion happens before wikilink resolution, in all four content compilers: @heroiclands/package-build/engine/journals, @heroiclands/package-build/sohl/items, @heroiclands/package-build/sohl/actors, and @heroiclands/package-build/engine/site-build (content-build site). That ordering is what lets a generated cell contain a wikilink. In the knowledgebase build it also runs outside that build’s code-fence protection — a query is a fenced block, so protecting it first would hide it from the expander. The expander itself is dependency-free ESM and is unit-tested in HeroicLands/content-build's tests/content-tables.test.ts``.