All notable changes to the Randomness plugin.
-
Single cells from a table in another note (asked on Discord):
`rdm:[[Names^npcs|xy]]`pulls one random cell out of a multi-column table living in a different note, and`rdm:[[Names^npcs|Trait]]`pulls one from a named column. Both halves already worked separately; this pins them working together and documents it. The point of a wide table is that a 5 x 20 grid costs far less screen than a hundred-row column — now every NPC note can reach into one without carrying a copy. -
The
.columnspelling works inline:`rdm:[[Names^npcs.xy]]`and`rdm:[[Names^npcs.Trait]]`now mean the same as the|columnforms, matching the[@npcs.xy]call you already write inside a note. Obsidian block ids can't contain a dot, so there is nothing ambiguous about it. -
Tables can roll on tables in other notes. A table cell may hold a whole roller span —
`rdm:[[Bestiary^monsters]]`or thedice:spelling — and it rolls as part of the result, pulling the note it names into scope by itself. NoUse:line, no second copy of the table: build one Bestiary and let every encounter table in the vault borrow from it. Repetitions, column picks and|sep:glue all work inside a cell. Two notes whose cells roll on each other are fine — that's a sheet, not an import loop — and a cell naming a note that isn't in the vault fails only on the row that uses it, naming the note and table it wanted.The note you name is the note that answers: the call resolves against that note's tables, so a same-named table in the rolling note can't stand in for it. (Notes are matched by name, as Obsidian's own links are, so two notes with the same filename in different folders are one name here.)
Dice Roller did this by re-rendering results as markdown. We don't revive spans in results — it breaks lock targeting — so the span is translated when the table is read instead.
dice:accepts the column pick inside the brackets. Dice Roller wrote the pick after the link (`dice:[[Note#^npcs]]|xy`) whilerdm:— and Obsidian's own link syntax — put it inside (`dice:[[Note#^npcs|xy]]`). The second spelling raised Unrecognised wikilink. Both are accepted now, under both prefixes, with or without the#before the block id.- Guide chapter 02 pointed at notes that don't exist. Two of its
live example rolls named a
Lootand aNamesnote, so a reader who installed the guide met an error on the page teaching them cross-note rolls. They point at the guide's own tables now, and a test runs every cross-note span in the guide against the guide. - Six raw NUL bytes in the source tree. Three view files used a
literal NUL as a cache-key separator instead of the
\0escape, which made git treat them as binary — no diffs in review, and they tripped the release NUL sweep every time. Same bytes at runtime, text files again.
- Sortable Favourites (#15): the Favourites section in the Generators tab gains a ⇅ button on its header that cycles the view between Pin order, Name A→Z and File A→Z, and ▲▼ buttons on each row for putting favourites exactly where you want them. Sorting is a view over your pin list, not a rewrite of it — switch back to Pin order and the arrangement is as you left it. The chosen sort persists across reloads.
- Resizable result panel (asked on Discord): the sidebar's Generators tab now has a drag grip on the top edge of the "Last roll" panel. Drag it to give long results more room (or shrink the panel out of the way); the height is remembered across reloads. Double-click the grip to return to the default 40% cap. The tree always keeps some space above the panel.
- Hideable sidebar tabs (#14): Settings → Randomness → Sidebar tabs has a Show … tab toggle for each of Generators, Decks, Portraits, Builder and Dice, so a table-only user can trim the pane to what they click. Open panes update as you toggle; a lone remaining tab drops the tab bar; hiding all five keeps Generators so the pane is never empty. Commands that open a specific tab (Open dice tray, Open portrait roller, Open portrait builder) still work with that tab hidden — it reappears until the pane is closed, leaving the setting alone.
- Hover previews in the sidebar (asked on Discord): links in
the sidebar's result panel and deck cards, and in the .ipt
reader, now open Obsidian's Page preview popover on hover, the
same as links in a note. Page preview only watches its own
Markdown views, so the plugin's views now raise the
hover-linkevent themselves and register as a source — a Randomness toggle appears under Settings → Core plugins → Page preview, where you can choose whether hovering needs Ctrl/Cmd (the default, matching notes) or fires on its own. Links rendered inside notes are untouched; the Markdown view already previews those.
- Dungeon grids — roll a whole map from a deck of dungeon
cards:
deck:Dungeon|2x2(or3x2|150for 3×2 at 150 px tiles) in a ```randomness codeblock deals W×H tiles laid out as a grid, each turned its own way per the deck's Turn settings (quarterdecks land any of four directions with equal odds, `half` decks upright/upside-down, Turn chance 0 keeps tiles upright — the chance is an on/off switch for grids). 🎲 Roll returns the current tiles to the deck at random positions before dealing, so rerolling never eats the deck; hovering a tile offers a per-tile reroll (🎲) and a manual turn (↻); 📋 Copy grid copies the map as rows of image embeds. - Guide chapter 10 — Portraits (issue #12, from a contribution
by immortel32): the portrait feature's own guide chapter — setup,
blocks, inline spans, seeds, packs, a full parameter reference,
the recipe JSON shape, and troubleshooting with the real error
messages. Fact-checked against the code before adoption: the
contributed draft's per-card 🎲/📌 controls don't exist (blocks
have one grid-level ⟳ Reroll plus per-tile lock and PNG icons;
layer-by-layer tweaks are the Builder tab), and PNG replaces
the block with an
![[image]]embed — the chapter now says so. Chapter 09 and Start Here link to it.
- Graphical dice now roll for aliases (issue #9, from TGSlasher).
dice:ability|renderand an alias typed into the Dice Tray's formula box skipped the 3D dice and fell back to a plain roll, because the animation eligibility check looked at the raw text ("ability") instead of the formula the alias stands for — only the Saved buttons, which pass the formula directly, animated. Both paths now resolve aliases first (a|renderor|norendersaved inside the alias's own value is honoured too), and the roll breakdown shows the real formula. Also fixes multi-flag spans likedice:2d6|form|render, where only the last flag was stripped before the eligibility check.
- Community generator: D&D spellbooks & scrolls (SRD 5.2) by
Brinx-git (issue #7) — weighted wizard/cleric/bard/druid spell
tables in
community-generators/dnd-srd52-spells/, with a CI test guarding the file against parse-breaking edits. Alphabetized books were already covered by the built-in>> sortfilter:`rdm:[@8 Spellbook >> sort >> implode \n]`.
- Deal multiple cards, straight into the note (issue #8, from
GilgameshofUT). Every deck call now takes an optional count and
embed width:
`deck:Poker|5`shows a Deal button that draws five at once (thumbnails inline, names for text-only cards) plus a 📌 that replaces the span with the hand as ordinary markdown —![[card.png|200]]embeds for cards with art (facing kept as trailing text),**Name (facing)** — meaningfor text cards.`deck-mod:Poker|5|200`is the dice-mod of decks: the first render deals once and bakes the hand in the same act, and the deck only advances if the note rewrite succeeds. The codeblock form takes the same count —deck:Poker|5deals five per click, shows them as a row of cards, and its 📋 Copy hand button copies the hand as markdown for pasting in Live Preview. Deck state tracking is unchanged throughout: one save and one change notification per deal, partial deals notice when the deck runs short, and rendering still never draws (the self-destructingdeck-mod:span being the one deliberate, dice-mod-matching exception).
- Quarter-turn decks. A deck can now turn its cards any of
four ways, for square cards with a reading on each edge (Story
Engine) or random map tiles. In the Decks tab, Turns picks
half(upright/reversed, as before) orquarter, and the old Reversed chance % field is now Turn chance % — the odds a draw is turned at all; a quarter-turned card lands onright,reversedorleftwith equal odds and its image rotates to match. In.rdmfiles,Turn: quarter(optionallyTurn: quarter 25%) does the same for in-generator decks and seeds a folder deck's defaults;Flip:still works and meansTurn: half. Card text can branch on{$facing}(upright/right/reversed/left) or the new{$turn}(0–3 quarter turns clockwise). Existingdeck.jsonfiles need no change.
- In-note controls take
user-select: nonefrom the stylesheet.makeEditorSafe()— the helper that keeps a roll's 🎲 / 🔒 / 📌, a codeblock's Reroll and a deck's Draw clickable in Live Preview — set that property from JavaScript, which Obsidian's automated plugin review flags. It now adds arandomness-no-selectclass defined instyles.css. A class survivescloneNodeexactly as the inline style did, so the guard still holds when Obsidian copies rendered DOM into a widget of its own, and whole rendered results stay selectable so they can still be copied. - Timers belong to the window the view lives in. Deck-state
saves and the Decks tab's scroll restore called the bare global
setTimeout/clearTimeout/requestAnimationFrame; in a popped-out window those belong to a different window than the one showing the view. They now go throughwindow.*explicitly. @codemirror/stateand@codemirror/vieware declared dependencies. The Live Preview extension added in 1.19.0 imports them, but they were only present transitively. Both remainexternalin the build — Obsidian provides CodeMirror at runtime — somain.jsis unaffected.- Tidying with no behaviour attached: four type assertions that
did not change a type, an unused import, a redundant escape inside
a regex character class, a
const service = thisalias, and an untypednew Array()whose.fill()therefore passed an unsafe argument. Nothing in this release changes what the plugin does; all 1,610 tests pass unchanged.
-
Hidden:keeps helper tables out of the browser. A generator's entry table usually calls others that exist only to serve it, and the browser listed all of them — so the one table you actually wanted to roll sat buried among its own plumbing. PutHidden:under a table and it drops out of that list:Table: Hoard [@Coins] and [@Gems] Table: Coins Hidden: a purse of silverIt changes nothing else. The table still rolls, still answers to its name, still autocompletes, and the scripting API still sees it — this tidies a list, it does not make a table less usable.
Hidden: noun-hides one without deleting the line.Hiding the first table doesn't change which table the file rolls; that is still the first one declared. And a file whose tables are all hidden says so, rather than claiming to be empty. Requested by SerhiiDeianov in issue #6.
-
Inline rolls now work in Live Preview. They never have. A
`rdm:[@Loot]`or`dice: 1d20`sat in the editor as the raw text you typed, and clicking it just put your cursor in it and showed the backticks — which is what people have been reporting as "clicking flips me back to source mode". Only Reading view ever rendered them.The reason is that Obsidian offers plugins the block elements of a note — which is why ```randomness codeblocks have always rolled in the editor — but draws inline code spans itself and never offers those. Randomness now renders them through the editor directly, so a roll looks and behaves the same in both views: same result, same 🎲 / 🔒 / 📌 buttons, doing the same things. A roll you have already seen keeps its value when you switch views rather than re-rolling.
Put your cursor in a roll and you get your expression back, so they are still editable. Rolls inside fenced code blocks stay as text — a
```textblock showing example syntax is not meant to roll at the reader.
- Buttons rendered into a note no longer move the editor's cursor. Clicking a codeblock's Reroll, a deck's Draw or a portrait's Reroll counted as a click into the document, which in Live Preview can unrender whatever the cursor lands in. They are now sealed off from the editor. This was shipped in 1.17.1 as a fix for the inline problem above; it was not that fix, but it is a real one for the surfaces it covers.
-
A
randomnessblock can now use tables from the rest of its own note. A block calling[@Party]could not reach aTable: Partydefined in the block directly below it — the same note, visible on screen, and the roll failed with "Unknown table: Party". Inline`rdm:`calls had always been able to see the whole note; only codeblocks were sealed off, for no reason anyone would recognise as intentional. They now see the note's other blocks and its^block-idmarkdown tables, in either direction: a block at the top can call a table defined at the bottom.Tables are shared; directives are not. A
Prompt:,MaxReps:orSet:still belongs to the block that declares it, so no block can quietly change how another renders. On a name clash the block's own table wins, then anything itUse:s, then the rest of the note. Reported by huffn in issue #5. -
A table chosen by a prompt can live in another file.
[@{$Prompt1}]builds its table name while rolling, so there was no name for Randomness to go looking for beforehand, and the roll failed even with the file sitting in the generator root. A block that picks a table this way now treats each of itsPrompt:options as a name worth finding. The same report; it was the detail that a[when]…[@Party]workaround DID work from the generator root, while the prompt version didn't, that separated the two causes.
-
Clicking a button in Live Preview no longer throws you back to raw source. Live Preview is a text editor with rendered markdown drawn into it, so a click on one of our buttons was also a click into the document: the cursor moved there, and Live Preview did what it always does when the cursor lands inside something — it showed you the source. The button worked, but your roll turned back into
`rdm:[@thing]`under the cursor every single time, which made the whole plugin awkward to use in the mode most people write in.The cursor moves on mouse-down, before any click handler runs, so the controls now stop those events outright and mark themselves as somewhere a cursor can't go. This covers every button we draw into a note: an inline roll's 🎲 / 🔒 / 📌, a codeblock's Reroll, a deck's Draw, and a portrait's Reroll.
The result text is deliberately left alone — clicking it is still how you put the cursor into a roll to edit or delete it.
-
Keeping a result as plain text no longer disturbs the rest of the note. The edit replaced the whole document, which cost Live Preview its scroll position, folded sections and selection. It now rewrites only the roll itself.
-
Keep a result as plain text. Inline rolls now have a third button, 📌, next to re-roll and lock. It replaces the whole call with what it rolled — backticks, prefix and expression all gone — so the result becomes ordinary prose you can write around. Roll as many times as you like first; 📌 keeps whichever result you are looking at, including the formula when
|formis on.Lock and 📌 answer different questions. Lock says "this result is the answer, but keep the machine". 📌 says "this is now just words in my note". Ctrl+Z undoes a 📌 while the note is open; after that the expression is gone, which is the point. Requested by Nedreow in issue #3.
-
dice-mod:leaves plain text behind, the way it used to in Dice Roller. It rolled once and then locked itself, which left a`dice-mod:1d20|form⟹7`span sitting in the note wearing an unlock button that could not work: unlocking stripped the lock, the note re-rendered, and the span immediately committed itself again. Now it bakes on first render —`dice-mod:1d20|form`becomes the words1d20 → 7and there is nothing left to click.Spans locked by older versions are left exactly as they are; upgrading rewrites nothing. Unlocking one turns it into an ordinary
dice:call instead of handing it straight back to the bake, so it becomes a roll you can actually play with.
-
The reference and the guide now say what the engine actually does. Every example in both was executed against the engine rather than read, and about sixty of them were wrong. The documentation had been describing features that were never implemented: the filter list advertised
aandmid, neither of which exists, and gave the wrong argument shapes forreplaceandeachchar— all four failed silently, because an unrecognised filter returns your text unchanged with no error.MaxReps:was described as capping[@100 expensive_table]; it does not, and nothing does.|sep:was offered for[@N table], which ignores it — use>> implodethere. An embed example used a!setdirective that has never existed, so it rendered a broken link.The Settings reference had been truncated mid-sentence and shipped that way: three settings of eight, no defaults, and three labels that don't match what Obsidian shows you. It now lists all eight with their real labels and defaults, plus the buttons. Dice Roller compatibility is documented properly as automatic — on whenever the Dice Roller plugin is disabled — rather than "off by default", which it never was.
Also corrected: several dice modifiers do less than their names suggest (
s/sdonly reorder the displayed dice,uis skipped entirely when you ask for more dice than faces), a-=condition incsis tested first and wins, keep/drop always applies last whatever order you write the suffixes in, anddice: 1dsquietly rolls a d100 instead of a Genesys setback die. -
\apicked the wrong article in front of a rolled value.\a [@creature]was always "an", whatever the table returned, because the lookahead read the unevaluated call rather than its result. It now decides from the rendered text, and works across a table boundary — an item that is just\acan take its noun from the caller.
- The beginner's guide is rewritten for someone new to all of
this. The old version explained
^as "a block id" andprop:as "a template" — swapping one unknown word for another. Terms are now defined the first time they appear, in plain language, and the concepts that used to arrive undefined (block ids, frontmatter properties, codeblocks, variables, conditionals) are introduced before they get used. Chapter 1 warns you to turn Dice Roller off before its settings will do anything, and chapter 8 tells you the one thing that does not migrate by itself: saved formulas need pasting into Dice formula aliases.
- The documentation now runs in CI. Every generator example in the docs is executed on each build, and every filter and directive the docs name is checked against what the engine implements — reading both lists out of the source, so they cannot drift. A wrong example is no longer invisible to the test suite.
- Tag rolls on a big tag no longer only pick A-names. The vault
lookup returns matching notes sorted by path, and the roll took the
first 50 of that list before the engine picked one — so a tag with
1500 notes could only ever return notes from the top of the
alphabet.
|link,|linkpathandprop:rolls now consider every matching note: they build their result from the note's path and its frontmatter, so there is no file I/O to cap. Block rolls (rdm:#tagwith no mode) read each candidate note off disk, so they keep a 50-note ceiling, but it is now a random sample of the matches rather than the alphabetically-first 50. Seeded rolls stay reproducible — the sample is drawn from the roll's own seeded RNG. Reported by Gizmo734.
-
|sep:— choose what goes between multiple results. A roll that returns several things has always joined them with,, which is right inside a sentence and wrong everywhere else.|sep:sets the glue:`rdm:3#monster|sep:<br>|link`puts one monster per line,`rdm:3#monster|sep:<br>• |link`bullets them, and`rdm:3[[Rumours|line]]|sep: /\_`runs them together with slashes. It works on every multi-result inline roll — tag rolls, table rolls, line and block rolls — and under thedice:prefix.Everything after
sep:is the separator, spaces and HTML included. Escapes cover what the surrounding syntax would eat:\nnewline,\ttab,\_space (needed at the end of a glue, since an inline span reaches the parser trimmed —sep: /joins with" /", andsep: /\_is the one that gives/),\\backslash. On a wikilink rollsep:goes outside the brackets — inside them a pipe already means "column pick" — and on a tag roll it is a segment likeunique, so it comes beforeprop:, which still swallows the rest of the line. Separator text is never evaluated, so a glue containing[@table]prints as itself. Thanks to Gizmo734 for the request.
-
dice:*|folder=…rolls instead of silently doing nothing useful. The tagless*source — "any note matching these properties", with no tag constraint — never reached the tag-roll branch under thedice:prefix. It fell through to the formula translator and came out as the nonsense expression{*|folder=Bestiary|link}: no error, no roll, nothing to indicate the syntax was fine and the plumbing wasn't. The reference has claimed since these filters landed that they work under the compatibility prefix, so this was a documented feature that had never run.*now dispatches exactly like#tag, including the repetition prefix,|unique, andprop:templates, and a test pins the two prefixes to the same note on the same seed. -
Documentation:
[@N table]was described as joining results with blank lines. It doesn't, and shouldn't — a repeated sub-table call runs its results straight together, because it is nearly always embedded in a sentence (The party of [@4 hero] sets out.) where an injected blank line would be wrong. The separator is the author's call, via>> implode: bare for,, or\n/\n\n/<br>/;\_for anything else. The reference's "Call variations", "Filters" and "Repetitions" sections now say so, with a new note on the one place separation is automatic — a whole-fileMaxReps:roll, whose reps are standalone blocks and so are blank-line separated. Behaviour is unchanged; the corpus tests now pin every one of these joins so the docs can't drift again.Two smaller errors went with it: a filter's glue is trimmed and never quoted, so the documented
implode ", "would have put the quote marks in the result andimplode ,loses its trailing space. Both examples are corrected, and\_is documented as the way to end a glue with a space. Thanks to claudermilk for the report.
-
A bare dice formula in an
rdm:span now rolls.`rdm:2d10`used to render the literal text "2d10" — no roll, no error, nothing to suggest anything was wrong. It was the trap waiting for anyone convertingdice:spans, where the formula is the whole expression. An inline call that is nothing but a dice formula is now treated as`rdm:{2d10}`, and an omitted die count is filled in (`rdm:d20`) for the same reason.The test is deliberately narrow: the whole expression must be a formula, so
`rdm:you take 2d6`is still literal text, and no expression containing generator syntax ([@table],{…},#tag,|) can match. Bracing is native, so`rdm:3d6>=10`means what`rdm:{3d6>=10}`means — 1 when the total reaches 10 — rather than the success-counting adice:span would read it as. Thanks to Anna_B_Meyer for walking into it.
- A "Show dice formula" setting. Inline rolls can now show what
was rolled next to the result —
2d6+3 → 11instead of a bare11— without adding a flag to every call. Settings → Randomness → Show dice formula, off by default. It covers both prefixes, so`rdm:{2d6+3}`and`dice:2d6+3`read the same (the{…}wrapper is dropped from the display), and it only touches rolls that actually rolled dice, so`rdm:[@Weather]`is unaffected. Adice:span still overrides it per roll:|formshows the formula with the setting off,|noformhides it with the setting on, and|text(label)wins over both. The formula is display only — locking a roll commits the result, as before.|noformhad been accepted and ignored until now. Thanks to Anna_B_Meyer for the request.
- Roll more than one note at a time. Tag and folder rolls take the
same repetition prefix the
[[Note|line]]rolls have always had —`rdm:3#monster|link`or`rdm:{1d4}#rumour`— and the new|uniquesegment draws them as a deck so no note comes up twice:`rdm:3#monster|unique|link`is three different monsters. Comma-joined, works withprop:templates (`rdm:3*|folder=Bestiary|unique|prop:{{name}} (CR {{cr}})`), and passes through thedice:prefix.|uniquehas to come beforeprop:, which swallows the rest of the line. Asking a unique roll for more notes than match gives you all of them rather than an error. Thanks to Gizmo734 for the suggestion.
api.rollFormula()andapi.formulas()— saved dice formulas are now reachable from scripts. Formula aliases (Settings → Randomness → Dice formula aliases, and the dice tray's ★ button) resolved only for inlinedice:spans and the tray, so a Templater / Meta Bind / QuickAdd script had no way to invoke one — the reported case was an initiative button that rolls exploding step dice and writes the result to a monster note's frontmatter.api.rollFormula("sneak")now rolls the saved alias (matched trimmed and case-insensitively, exactly as inline), and any unmatched string is rolled as a raw formula in the full Dice Roller grammar — modifiers, special dice,[[Note^id]]table rolls,#tagrolls.api.formulas()lists the saved aliases.roll(),rollUnscoped()androllExpression()are unchanged: they still do not resolve aliases, so an expression sharing a name with one keeps its existing meaning. API version 1.2.0 → 1.3.0 (additive).- Print a rolled note's properties, not just a link to it. Tag and
folder rolls could always filter on frontmatter
(
rdm:*|folder=Bestiary|cr=3|link); now they can output it. End the call withprop:and the rest is a template:`rdm:*|folder=Bestiary|prop:{{link}} — CR {{cr}}, {{hp}} HP`→ [[Bestiary/Bog Hag|Bog Hag]] — CR 3, 45 HP. One note is rolled and the whole template is filled from it, so the values can never belong to different monsters the way two separate`rdm:`spans would.prop:cris shorthand forprop:{{cr}}.{{link}},{{linkpath}},{{path}}and{{name}}describe the note itself; every other placeholder is a frontmatter key, and naming one also requires it, so a note missing that property is never picked. Works under thedice:prefix too. api.randomNote()now returns the note'sfrontmatter, so Templater and dataviewjs callers don't need a secondmetadataCachelookup. API version 1.3.0 → 1.4.0 (additive).
count(Table)inside{...}— how many items a table has. Mostly useful for rolling an index that stays valid as a table grows:{1d{count(npcs)}}instead of hard-coding1d3and forgetting to update it when you add a row. The argument is a table name rather than a value, so dotted column names (count(npcs.Job)), quoted names with spaces, and interpolated names (count({$whichTable})) all work. It counts items, so a lookup table with two range rows counts 2, not the span of its dice formula; counting a table that doesn't exist is an error, the same as rolling one.
- How to pull several columns from the same rolled row. Each
column of a markdown table is its own rollable table, so
[@npcs.Job]and[@npcs.Name]land on different people — which looks like a bug the first time you write a sentence with two of them in it. The fix is to roll the row number once and pick by index:The [#{row=1d{count(npcs)}} npcs.Job] was [#{$row} npcs.Secret]. Written up in the reference (Rolling on note content → One row, several columns) and in guide note 02, along with the reason it can't be split across two`rdm:`spans: every inline span is evaluated on its own, so variables don't carry from one to the next. - The
[#table]current-index form — pick the item at the position of the item currently being rendered, for cross-indexing parallel tables — is now in the call-variations table, and the function list (if,max,min,round,substr, …) is documented at all.
- The dice animation now shows the total when a roll has a
modifier. The graphical dice overlay only drew its
= totalline when a roll had more than one die, so a single die plus a modifier —1d20 + 5,1d6 - 3— animated the die and showed nothing else. The die face was the only number on screen, which read as "the modifier was ignored"; the roll itself was always correct, and the dice tray's result panel had the right number the whole time. The total is now shown whenever it says something the faces don't: more than one die, or a flat modifier or dropped die that makes the total differ from the dice on screen. A plain1d20still animates without a redundant total. Thanks to Anna_B_Meyer for the report.
names.rdmin the Fantasy Hub bundle — a much larger NPC name pool. Dictionary tables keyed byrace_gender([#elf_female TF-PersonName], orrollUnscoped("TF-PersonName", { dictKey: "elf_female" })), plusTF-FirstNameandTF-Surnamefor the halves. Surnames blend a curated list with a prefix + suffix compound table, giving roughly 16,000–47,000 full names per race/gender bucket instead of a few hundred. Covers human, elf, half-elf, half-orc, gnome and goblin; half-elves draw from both parent cultures. Town Forge's place templates use it to keep every NPC in a settlement distinct.
- Bigger built-in portrait name tables. The names behind
api.portraits.roll()came from lists of 9–15 entries per race, so a town full of NPCs would occasionally hand out the same name twice (human males had only 210 possible full names). Each list is now 32–48 entries — human is 48 × 48 — cutting the chance of a repeat across a 40-NPC town from roughly 36% to 4%. This is the fallback path now that the Fantasy Hub shipsnames.rdm. - Fantasy Hub
Personalitygrown from 21 to 61 beats, so characters stop sharing a personality line two or three times per generated town.
- Reroll button on
randomnesscodeblocks. Fenced```randomnessrollers now render a Reroll button beside their prompt controls, so an NPC or table roller embedded in a note can be re-rolled in place — the same affordance the.rdmfile view and inline calls already had. Prompt selections are preserved across rerolls, and the button forces a fresh roll even when Stable codeblock seeds is on (the new result then persists across passive re-renders until the next reroll).
- Tag and property
|linkrolls now show the note's name, not its full path.`rdm:#tag|link`and property/folder variants like`rdm:*|spelllevel=1st|link`render the wikilink as the note's name —Burning Handsrather thanSpells/Level 1/Burning Hands— while still linking to the correct note wherever it lives in the vault. This is the new default for every|linkroll.
|linkpathflag for link rolls that keeps the full vault path visible, for anyone who preferred the previous display. Works under thedice:compatibility prefix too (`dice:#tag|linkpath`).
- Copy button on Android pasted a
tempNNNN.htmlattachment link instead of the result. The sidebar's Copy button used to put an HTML flavour on the clipboard and let Obsidian convert it to markdown on paste. Android's WebView hands that flavour back as a file, so Obsidian saved it into the attachment folder and linked to it. Copy now converts to markdown itself and writes plain text only — formatting is preserved on every platform, and there's no HTML flavour left to be mishandled.
- Folder filters on tag rolls.
folder=is a reserved filter segment restricting candidates to notes under a folder (recursive; comma for OR) — combine with tag and property filters:`rdm:*|folder=Bestiary|cr=3|link`rolls a random CR 3 monster note, ready to drop into anencounter:line. - Visible re-roll die on API-shim rollers. Encounter counts
rendered through the
window.DiceRollershim (Initiative Tracker's1d6: [[Monster]]) now show a small die icon; clicking the count re-rolls it.
- Dice breakdown — see what each die rolled. The engine now reports
every die's face alongside the sum (Ironsworn challenge dice, stat
arrays, …):
- Hover any inline roll for the per-die breakdown
(
4d6dl1 → 5, 3, (1), 6— dropped dice in parens, explosions!, re-rollsr). Always on; works forrdm:anddice:spans alike, including dice inside table results. - Settings → Randomness → Show dice breakdown appends the faces to
the visible result —
13 (7, 6)instead of just13. Individualdice:spans can opt in with the new|diceflag instead. - Locks commit the faces when they're visible, so the record survives in the note.
- Dice tray history shows the breakdown under each roll, and the big result's tooltip carries it too.
- For scripts:
EvaluatorOptions.onDicereceives each dice term's notation, per-die detail, and total during evaluation.
- Hover any inline roll for the per-die breakdown
(
- Deck display blocks. A
```randomnesscodeblock whose whole body isdeck:Namerenders the deck's last-drawn card at full card size with a 🎴 Draw button and remaining count — the big-card companion to the compact inline span. Rendering never draws. - Card copy buttons. Hovering a drawn card (Decks tab or deck
block) shows portrait-style icon buttons: copy the card image as an
![[embed]], copy a ready-to-paste deck block, copy the card as text, or copy the inlinedeck:span. - Weather example deck. Ten illustrated cards for day-by-day weather: upright passes by nightfall, reversed settles in until the next card (reversal chance preset to 40%). Downloadable from settings alongside the playing cards and tarot decks, which now share one compact "Example decks" row.
- Initiative Tracker / Fantasy Statblocks encounter counts.
encounter: 1d6: [[Monster]]no longer collapses to a flat 1 when Dice Roller is disabled mid-session: thewindow.DiceRollershim now takes over live when Dice Roller unloads (and when the compat toggle flips), and its rollers gained theisStatic/containerElsurface the encounter line renders — including click-to-re-roll counts. - Decks tab quality of life. Decks collapse to their title row (click the title; persisted, with Collapse all/Expand all), the tab no longer resets its scroll position on every draw, and drawn cards no longer flash their text while the image loads.
- Tag rolls can filter by tags AND/OR frontmatter properties. Extra
pipe segments narrow the candidate notes — ideal for multi-universe
vaults:
`rdm:#npc|universe=Eldara|link`— only notes whoseuniverseproperty is Eldara.`rdm:#npc|#merchant`— both tags required;#npc,#monster— either tag.`rdm:#npc|universe=Eldara,Vex`— property is Eldara OR Vex;universe=*— property exists with any value.`rdm:*|universe=Eldara`— filter by property alone, no tag.- Matching is case-insensitive; list-valued properties match if any
entry hits; wikilink values (
universe: "[[Worlds/Eldara]]") match their target's name or alias. Works under thedice:compatibility prefix too. Still metadata-cache only — no Dataview required.
- Persistent decks. Deck state can now survive across rolls, notes, and
restarts (design:
docs/persistent-decks-design.md):- Folder decks — each folder under
<Generator Root>/Decks/<Name>/is a deck. One image = one card; an optional.rdmfile (Type: Dictionaryrecommended) adds card text, paired to images by filename ↔ key (the-tower.png↔The Tower). A_back.*image is the card back. Settings + state live indeck.jsoninside the folder, so a deck travels (and syncs) with its state. [!deck:Name]draws from a folder deck inside any generator; thedeck:prefix keeps decks out of the table namespace.Shuffle: deck:Nameresets one.deck:Nameinline spans in notes render the last drawn card with a 🎴 Draw button. Rendering never draws — only the explicit click does, so scrolling a note can't burn cards. The same rule applies everywhere: passive codeblock re-renders draw from a throwaway copy; explicit actions (browser Roll, inline re-roll, Decks tab, commands) commit.Deck: persistenttable directive — an in-generator table's deck-pick state persists (stored in the plugin folder'sdeck-state.json).Flip: N%table/deck orientation — each draw sets{$facing}toupright/reversedso card text can branch (tarot-style), and reversed card images render rotated.- Decks tab in the browser pane: per-deck Draw / Peek / Draw & bury / Undo / Shuffle, remaining count, reversal-chance setting, and draw history.
- Commands: "Draw a card from a deck" and "Shuffle (reset) a deck".
- Example decks in settings — downloadable on demand (never bundled, to keep the plugin small): a standard 54-card playing deck and the public-domain Rider–Waite–Smith tarot with Waite's 1911 upright/reversed meanings, reversal preset to 50%.
- Folder decks — each folder under
- Oracle/lookup tables work with a plain die header — no
dice:syntax needed. A two-column table headedd6,D6,d100, ord%(not just the explicit`dice: 1d20`form) is now rolled correctly: the header is normalised to a formula the engine can actually roll (d6→1d6), so the lookup returns the matching row instead of silently coming back empty. The lookup is only built when the header normalises to a real die, so a plain label header stays an ordinary table rather than a broken lookup.
- Dice Roller
|formand|textflags now survive a re-roll. The formula (|form) or label (|text(…)) was only applied on first render; clicking 🎲 re-roll collapsed the span to the bare rolled number. Both display flags are now re-applied on every roll. - The
|formflag no longer leaks into the shown formula. A2d6+3|formroll now displays2d6+3 → 11instead of2d6+3|form → 11.
- A broken
Use:no longer breaks every inline roll in a note. A single unresolvableUse:in onerandomnesscodeblock used to make every inlinerdm:call in the same note render a "target not found" error. Now a plainrdm:[@table]call ignores an unrelated broken import and still rolls; only explicitrdm:[[Note]]and#tagcalls report a genuinely missing target. - Documentation examples stay inert. A
randomnesscodeblock shown inside a larger display fence (e.g. a ````text wrapper) is no longer extracted and run, and inlinerdm:/`dice:` spans inside fenced code blocks are no longer evaluated — so guide and reference examples display instead of executing.
- Installing Fantasy Hub content no longer sets off Templater prompts. The templates destination is now added to Templater's excluded-folders list before the bundle is written, so Templater's "trigger on new file creation" no longer executes the templates as they land — which was prompting for town/size and could overwrite the templates.
- The Fantasy Hub Start Here note rolled
[@FantasyShop], a table that does not exist — corrected to[@TF-Shop], clearing the "Unknown table: FantasyShop" render error.
The Dice Roller merge, complete (phases 1–7) (see
docs/dice-roller-merge-plan.md). Dice mechanics and syntax ported
from @javalent/dice-roller (MIT, © Jeremy Valentine).
- Fantasy Statblocks support. Randomness now provides the
window.DiceRollerAPI surface that Fantasy Statblocks (and other Dice Roller API consumers) integrate with —registerSource,getRollerString,getRollerSync/getRoller,parseDice, and thedice-roller:loadedevent. Statblock attack and damage dice keep rolling after Dice Roller is disabled. The shim never installs while the standalone plugin is enabled. - Dice modifiers on any
{NdN}term: keep/drop (k,kh2,kl2,dl1,dh1), exploding dice (!,!!,!3,!i), re-rolls (r,r3,ri), sort (s,sd), unique (u), and success counting (cs>=5, with-=Nscoring −1). Explode and re-roll accept optional conditions ({1d6!i=!3},{1d4r<3}); conditions chain and are OR'd.{4d6dl1}and{2d20kh}+5finally work everywhere the engine rolls — codeblocks, inlinerdm:, and the JS API. - Special dice: percentile
{1d%}, digit dice{1d66%}(Traveller d66), Fudge/Fate{4dF}, and custom face ranges{1d[3,5]}. - Roll on markdown tables and lists. Any table or list in a note
with an Obsidian
^block-idis now a rollable table. Inline:`rdm:[[Note^taverns]]`(with lock/re-roll buttons); from codeblocks and generators:Use: [[Note]]then[@taverns]with reps, filters, and deck picks. Multi-column tables expose[@id.Header]per column and[@id.xy]for a random cell (inline:|Header/|xy). Two-column tables with a dice-formula header (dice: 1d20) act as lookup tables, ranges like1-2,11, and13,14included. Cells are raw generator syntax, so{2d6}and[@OtherTable]inside a cell just work. - Dice Roller compatibility (
dice:inline rolls). A new settings toggle routes inlinedice:code spans — plusdice+:,dice-:, anddice-mod:— through the Randomness engine with Dice Roller's own syntax: bare success conditions (3d6>=5counts successes, as Dice Roller defined it), omitted values (d20,3d→ d100s), all modifiers and special dice, and table rolls3[[Note^id]]/1d4+1[[Note^id]]/|Header/|xy. Everydice:span gets Randomness lock/re-roll buttons — locks replace Dice Roller's fragile result saving anddice-mod:.|text(label)shows the label with the rolled value in a tooltip,|formshows the formula with the result, anddice-mod:spans write their roll into the note on first render (as a lock — the durable form of Dice Roller's note-modifying roll).|renderplays the graphical dice animation; the remaining display flags (|nodice,|avg,|none,|noform) are accepted and currently inert. Formula aliases from settings work too: definesneak = 4d6dl1under Settings → Randomness → Dice formula aliases anddice: sneakrolls it. Not yet supported (clear errors): stunt and Genesys narrative dice. The toggle defaults to ON when the Dice Roller plugin isn't enabled and OFF while it is (an explicit choice always wins); enabling it alongside an active Dice Roller shows a warning — one plugin at a time should own the spans. Flipping the toggle re-renders open notes immediately. - Dice tray. A right-sidebar tray (dices ribbon icon, or the
"Open dice tray" command) replacing Dice Roller's Dice View: tap
d4–d100 buttons to build a pool (right-click removes), toggle
advantage/disadvantage (each d20 becomes
2d20kh/2d20kl), step a flat modifier, and Roll. A formula box takes the full Dice Roller syntax — modifiers,[[Note^id]]table rolls,#tag, aliases — scoped to the active note so[@Table]works too. Formulas saved from the tray land in the same store as the "Dice formula aliases" setting, so a tray-savedsneakalso rolls asdice: sneakin notes. Click a history row to re-roll it; click the result to copy. - Graphical dice. Rolls can animate: a tumbling 3D cube for
d6s, spinning polyhedra with a slot-machine number cycle for the
rest, settling on the rolled faces (dropped keep/drop dice shown
dimmed, total badge for multi-die rolls). Plays in the dice tray
and for inline rolls with the
|renderflag; click to dismiss. Purely decorative by design — the engine rolls first and the animation replays those exact values, so seeds and locks are unaffected, and there are zero new dependencies (the three.js physics port was rejected: Obsidian plugins can't lazy-load chunks, so it would have permanently ~5×'d the bundle for an animation). Toggle under Settings → Randomness → Graphical dice. - Beginner's guide (installable). Settings → Randomness → "Install the guide" writes a "Randomness Guide" folder of ten short notes — one per feature, kid-friendly, every example live and rollable — from "roll a die" through generator files. Re-running refreshes the notes. Sourced from docs/guide/ (npm run embed-guide).
- "Create new generator file" command. A .rdm file is just a text file, but Obsidian can't create one and manual renames trip over hidden Windows extensions — this command creates a starter generator (in the Generator root when set), uniquely named, and opens it for editing.
- README rewritten for the merged plugin: 30-second tour, dice/tables/locks/compat up front, learning path, migration pointer.
- Migration guide & retirement kit. docs/migrating-from-dice-roller.md walks Dice Roller users through the (three-step) switch, and docs/retirement/ holds the ready-to-paste README banner, final release notes, and deprecation-notice patch for winding down the dice-roller repository.
- Roll random lines, blocks, and tagged notes (no Dataview
needed).
rdm:[[Note|line]]rolls a random line from a note,rdm:[[Note|block]]a random block (paragraph, heading, fenced code…); repetitions work (rdm:3[[Note|line]]).rdm:#tagrolls a random block from a random note carrying that tag (frontmatter and inline tags, nested tags included), andrdm:#tag|linkinserts a link to a random tagged note — all backed by Obsidian's own metadata cache. Tag picks happen inside the engine, so seeded rolls stay deterministic and re-rolls re-pick the note. Indice:compat,[[Note]],[[Note]]|line,#tag,#tag|-, and#tag|linknow work (block-type filters like|paragraphapproximate to the block roll; the every-file#tag|+mode errors clearly). - Wikilinks resolve like Obsidian links.
Use: [[Note]],rdm:[[Note^id]], and codeblock imports now fall back tometadataCache.getFirstLinkpathDest, so a shortest-path link finds the note anywhere in the vault — not just relative to the calling note or the Generator root. - Repetitions on inline wikilink rolls.
rdm:3[[Note^id]]andrdm:{1d4+1}[[Note^id]]roll multiple results, joined with ", ". - Wikilink
Use:targets.Use: [[Note]]/Use: [[Note^id]]resolve like Obsidian links written as paths — relative to the calling note's folder, then the Generator root, then vault-rooted.
- Large notes render far faster. Every inline
dice:/rdm:span rebuilds its scope by parsing the whole note, so a big sheet (2,000+ lines, hundreds of spans) re-parsed itself once per span — several seconds of stalls on load. The note-table extraction and codeblock scan are now memoised by content, so the note is parsed once per render instead of once per span (measured ~68x faster on a 740-span note: 3.5 s of parsing down to ~50 ms). Results are unchanged; an edit is a natural cache miss. - Table auto-discovery no longer re-scans the whole note per span.
Resolving
[@table]references parsed every table cell in scope on every span; the per-table reference set is now cached by table identity (another ~12x on the same note). - A block's rollers fill together. Inline spans in a block are now evaluated concurrently instead of one after another, so a big table populates in one paint rather than visibly ticking down row by row. The vault index dedupes concurrent warm-ups so the parallelism doesn't trigger redundant rescans.
- Padded lookup tables no longer leak their range keys. Sheets
that pad every row with a trailing empty column
(
| 01-30 | Creature, resident | |) failed lookup detection (which required exactly two columns) and fell through to the multi-column path, which joined the key cell into the result — a roll showed "01-30, Creature, resident" and self-referential reroll tables stacked keys ("35-36, 01-30, Creature, resident"). Trailing columns that are empty in the header and every row are now trimmed before the table shape is decided. - Lookup headers written as code spans now parse. Real sheets
write the dice header as
`dice:1d100`; the backticks were fed straight into the engine, erroring x150 with "unexpected character '`' at position 0". Code wrapping is now stripped before the roll expression is read. - Paragraph blocks with a
^block-idare rollable one-item tables. Dice Roller rolled any block, and real sheets use small paragraph blocks as aliases (^encounter-underworld-lawful-daywhose only content is another roll). Such blocks now resolve instead of being skipped. - Embedded rollers that point back at their own note roll. A
`dice:[[This Note#^id]]`span inside a cell now translates to a direct engine call ([@id], reps and column picks included) so nested rollers actually roll. The self-note match is case-insensitive, matching Obsidian's link resolution ([[encounter tables]]finds "Encounter Tables"). - Untranslatable embedded rollers degrade instead of erroring.
Cross-note
`dice:[[Other^id]]`spans and unsupported syntax keep their literal text but lose the backticks, so the engine's content parser shows the span verbatim rather than erroring the whole cell. - Unlock now works on duplicated expressions in Live Preview. Live Preview renders each row/widget separately (with no section info), and a locked span could pair with its first UNFILLED twin in the source — so clicking unlock targeted the wrong occurrence and did nothing. Pairing now requires the locked value to match.
- Locked spans now show an unlock icon instead of the dice icon — clicking it strips the lock and rolls a fresh preview, same behaviour, honest icon. Same button slot, so no mouse-chasing.
- Markdown backslash escapes in results render correctly:
\*shows a literal*(footnote markers like "5 sp **") instead of a visible backslash. Escapes inside code spans stay byte-literal. - Identical expressions repeated the same result across a note.
The engine's default RNG seed was
Date.now()— a note render evaluates every span in the same millisecond, so time-identical seeds made every copy of an expression land on the same pick (eight "Grinning Oak" taverns). Unseeded evaluations now draw their seed fromMath.random(); explicit seeds are unchanged. - "Lock all" now commits each occurrence's own on-screen value instead of copying occurrence #1's result to every duplicate.
- Cross-note rolls now prefetch their target.
dice: [[Note^id]]injects itsUse:line at bundle-build time — after the async prefetch had already run — so the target note never entered the resolver snapshot and every cross-note lookup failed with "Use: target not found". The prefetcher now walks direct-wikilink targets (and their ownUse:graphs) explicitly. dice:spans embedded in table cell text roll as part of the result ("Bustlingdice:1d8+5x # Inn Rooms") — Dice Roller revived them via MarkdownRenderer; we translate pure formulas into engine dice at extraction time.- Lookup tables with bolded keys (
| **1** | Braised beef |) are recognised — authors habitually bold the dice column, and Dice Roller tolerated it. Emphasis/code wrapping is stripped from key cells before range parsing. - Dice Roller compatibility is now truly automatic. 1.3.0 draft
builds computed the compat default once at load and then saved it,
so disabling Dice Roller later did nothing and
dice:spans rendered as plain code. The decision is now evaluated live on every render: no explicit choice → compat is on exactly when the Dice Roller plugin is disabled. The settings toggle now writes an explicit choice (newdiceRollerCompatChoicekey; the baked legacy key is dropped on load). - All Dice Roller display flags are tolerated:
|paren,|noparen,|round,|floor,|ceil,|noround, and|signedno longer error (they strip cleanly; rounding/sign display remain inert). This is also what Fantasy Statblocks appends to every roll. - README/CHANGELOG shipped with trailing NUL bytes in 1.3.0 draft builds; scrubbed.
- Self-imports are now a silent no-op.
Use:pointing at the file (or note) that contains it previously threw "Use: cycle detected" — and once notes hold rollable tables,Use: [[This Very Note]]is an easy thing to write. A file's own tables are already loaded, so the self-import just resolves to nothing. True multi-file cycles still error. - A note's own markdown tables are in scope for its inline calls.
rdm:[@taverns]works in the note that defines^tavernswith noUse:line, mirroring how same-note codeblock tables behave.
- Bare comparisons keep their IPP3 meaning:
{3d6>=10}still compares the sum. Success counting requires the explicitcsmarker. Every new suffix was previously a parse error, an unmodifiedNdNconsumes the RNG stream identically to before (seeded generators reproduce exactly), and1d[@table]nesting is unchanged. The full.iptcorpus passes untouched. - Markdown-content tables are additive: blocks without a
^block-idare ignored, and a plainrdm:[[Note]](no block id) still renders as an ordinary wikilink.
Feature release: reference tables across files without Use:, plus a
revamped, beginner-friendly example tutorial.
- Auto-discovery by table name. A
randomnesscodeblock or an inlinerdm:call can now reference a table by name ([@TavernName]) with noUse:line — the plugin finds the generator file that defines it anywhere under your Generator root and pulls it in automatically, following that file's ownUse:graph transitively. It is lowest-priority and purely additive: anything you define locally or import withUse:always wins, so discovery can never shadow your own tables. Previously this resolved only via the JS API. - Example tutorial covering all four usage styles. The "Add examples"
button now ships a guided, heavily-commented set — inline in a note, a
self-contained codeblock, standalone
.rdmgenerators, and referencing a.rdmfile from a note — plus a plain-language "Start Here" note.
- "Add examples" installs into its own
Randomness Examplessub-folder under the Generator root, so the tutorial stays grouped and is easy to remove in one move.
- Error messages are readable in every theme. Codeblock and inline error boxes used a red-on-red colour pairing that was unreadable in many dark themes; they now use a neutral panel background with a red accent.
- New
autoDiscoversuite (discovery, transitive discovery, the no-shadow guarantee, and dynamic/unknown references) plus updated bundled-examples coverage.
Maintenance release. No user-facing changes.
- Bumped esbuild to 0.28.1 to clear advisory GHSA-gv7w-rqvm-qjhr (dev-time only; the shipped plugin code is unaffected).
Bug fix release. Codeblocks that use `Use:` to import another generator and then call a table directly no longer render silently empty.
-
Codeblocks with `Use:` but no explicit `Table:` no longer silently produce empty output. The most common shape of a codeblock — `Use: foo.ipt` followed by one or more bare `[@SomeTable]` calls — was being parsed as zero tables, and the evaluator returned an empty string with no error. Authoring around this required adding `Table: Main` on the line above the call, which was a hidden requirement nowhere in the docs.
The fix: when the parser encounters orphan items (lines that aren't directives, before any explicit `Table:`), it now synthesises an implicit `main` table to hold them. The evaluator picks this up as the file's main entry and rolls it normally.
Files that already declare their main table explicitly see no change. The fix is purely additive — it makes previously-broken codeblocks Just Work without affecting anything that was working.
- 5 new parser tests covering the orphan-items cases (bare-after-Use, multiple-orphans, orphans-before-explicit, regression guard for files starting with Table:, degenerate Use:-only file).
- 4 new integration tests exercising the codeblock-with-Use scenario end-to-end through the evaluator. Total: 984 tests, all green.
Follow-up to 1.0.16, clearing the last two warnings from the automated review. No behaviour change.
vault.modifyno longer needs anas anycast. TheseedExampleGeneratorsflow narrowed aTAbstractFile | nullto aTFilevia duck-typing ("stat" in existing), which TypeScript can't follow — so the call site cast throughany. Switched toinstanceof TFile, which TypeScript's flow analysis recognises and narrows correctly. Same runtime behaviour; removes both the "unexpected any" and "unsafe argument" warnings.
Cleanup release addressing the warnings flagged by the Obsidian community plugin automated review of 1.0.15. No errors flagged in that review (1.0.15 fixed all blocking issues); this release clears the warning backlog so future submissions stay clean.
No user-visible behaviour changes — all fixes are lint compliance, type tightening, and dead-code removal.
document/window→activeDocument/activeWindowin 47 sites across nine view files. Obsidian'sactiveDocument/activeWindowglobals correctly resolve to the popout window's document when one is focused; baredocumentalways returns the main window. Behaviour is identical when no popout is open (which is the common case), but plugin UI created in a popout now wires up to the right document.globalThis→windowfor browser-API feature detection (crypto.randomUUID,ClipboardItem). These checks aren't popout-sensitive, sowindowis the right primitive.require()→ staticimportinfilters.ts. The lazy require was originally added to break a circular dependency withcontentParserthat no longer exists. Switching to static imports also eliminates the unsafe-anycascade that came fromrequire()returningany— about 15 lint warnings cleared in one change.catch (e: any)→catch (e: unknown)with a smallerrorMessage(e)helper. Same Notice text reaches the user; the type is now correct.- Unnecessary type assertions removed in three sites
(
as HTMLElement | nullafterquerySelector— fixed by usingquerySelector<HTMLElement>). - Promise handling in event listeners —
addEventListenerhandlers that did async work used to be declaredasync, which returns a promise the listener API silently drops. Replaced with synchronous handlers thatvoidthe inner async call, making fire-and-forget intent explicit. Plugin.onunloadis no longerasync(body had no async work; matches the base-class signature).
- Unused imports:
FilterCall,FilterValuefromevaluator.ts;PreviewRegistryfrominlineProcessor.ts. - Unused helper
folderOffromvaultIndex.ts. - Unused local
lineCountintableAutocomplete.ts. - Stale
eslint-disablecomments that are no longer reachable after therequire()→importchange.
- Unnecessary escape characters in three regexes (
\/,\&) and one markdown table (\|inside backtick code spans, where GFM treats inline code as opaque).
- New
jest.setup.tspolyfillsactiveDocument/activeWindowfor jsdom-based view tests. Obsidian provides these globals at runtime; jsdom doesn't, and view code now touches them at module init. The setup file aliases them to regulardocument/windowunder jsdom, which matches Obsidian's behaviour when no popout is open. All 975 existing tests pass unchanged.
Compliance release addressing all errors flagged by the Obsidian community plugin automated review of 1.0.14. The plugin was delisted pending a passing review; this release fixes each error individually. Warnings from the same review are left for a follow- up release that doesn't block listing.
revealLeafcalls are now awaited.Workspace.revealLeafreturns aPromise<void>in current Obsidian; we were calling it withoutawait, which both triggered the unawaited-promise rule and meant code after the call could execute before the leaf was actually revealed. Now properly awaited in bothbrowserViewandreferenceView.- Bumped minAppVersion from 1.4.0 to 1.7.2. The async
signature for
revealLeafrequires the newer API; the lint rule was correctly flagging that our declared compatibility was older than what we actually use. - Replaced
innerHTMLparsing withDOMParser. Two sites —sanitiser.sanitiseHtmlToFragment(HTML cleaning entrypoint) andbrowserView.htmlToPlainText(clipboard conversion). Both were already safe (detached documents, sanitised inputs), butDOMParseris the recommended pattern and doesn't trip the no-unsafe-innerHTML rule. - Replaced inline style with a CSS class. The browser pane's
click-to-copy cursor was set via
body.style.cursor = "pointer"; now uses a new `.randomness-clickable` class instyles.css. Matches Obsidian's plugin guideline that styling lives in stylesheets, not JS. - eslint-disable directives now include justification text.
The single
eslint-disable-next-lineinfilters.ts(lazy require to break a circular dep withcontentParser) now explains why it's there. Theno-consoledisable insettings.tswas removed entirely — the example-seeding diagnostics now surface in the user-facing Notice instead of the developer console.
The same review flagged ~80 warnings (unsafe-any in filters,
globalThis instead of window/activeWindow, document
instead of activeDocument, some unused imports). These don't
block listing but are real cleanup work; addressing them in a
follow-up release lets this compliance release ship quickly.
This is a substantial release covering real-world IPP3 compatibility, better first-run setup, and a thorough reference-guide rewrite. Most community generators that previously rendered empty or crashed should now render correctly, and new users can get from "just installed" to "rolling a working generator" in two clicks.
Six independent fixes uncovered while loading real community
generators (Dungeon_Room_Description.ipt and
Ultimate_Powers_Character_Generator.ipt).
-
Variable names are now case-insensitive.
{$Prompt1},{$prompt1}, and{$PROMPT1}all refer to the same value. IPP3 is case-insensitive for variable names; we were storing prompts as lowercase and accidentally treating mixed-case references as unset (empty string). Affects userSet:variables too —Set: Foo=xfollowed by{$foo}now works. -
Lookup tables without explicit
Roll:now auto-infer. IPP3 authors commonly omit theRoll:directive on lookup tables; the engine is supposed to infer1d<max-range>from the items. We required explicitRoll:and returned empty otherwise. -
[[when]…[end]](outer-bracket-wrapped conditional) now evaluates. When an IPP3 conditional is wrapped in an outer[…](a common idiom inSet:values), the engine could either infinite-loop or render empty. The content parser now detects whether[[…]]is an Obsidian wiki-link or an IPP3 wrapped expression by looking for structural markers ([when],[do],[else],[end],[@,[#,[$) inside the bracket pair. Wiki-links continue to pass through unchanged. -
&line continuation now respects directive boundaries. Per the IPP3 manual,&continuation is for table item lines. Some community files put&after aSet:directive too, which caused the engine to suck following body content into the Set's value and emit nothing.Set:,Define:,Roll:,Type:,Table:,Use:,Prompt:, and other directives now terminate at end-of-line; only item lines continue across&. -
Arithmetic on variables now adds numerically. When two variables hold numeric strings (the form
Set: A=5produces), expressions like{{$A}+{$B}}now compute8rather than concatenating to"53". Explicit string literals like'5'+'3'still concatenate, preserving documented behaviour. -
Marker-form literal_bracket no longer infinite-recurses. A defensive guard in the
literal_bracketrender path that previously triggered on any text starting with[now checks for exact marker text ([when],[when not],[do],[else],[end]), so genuine wrapped expressions re-parse correctly while stray markers emit as literal text.
-
Error messages in
.iptviews are now readable. The error bar was painted with red text on a red background, making the message invisible. The bar now uses the normal text colour against a muted background; the red is preserved on the left border and heading so it still reads as an error at a glance. -
Missing-
Use:errors are actionable. Files that depend on.iptfiles not in the vault now display a hint suggesting the user download the referenced file from the community pack, and noting that Randomness finds files by name anywhere in the vault. -
The "red error" in the in-app reference guide is gone. Five places in the reference used inline triple-backticks to represent a
randomnesscodeblock visually. Obsidian's reader sometimes parsed those as actual fenced codeblocks withrandomnessas the language, which then triggered the plugin's codeblock processor to render an error inside the reference view. All five rewritten to use single-backtick inline code. -
Reference guide syntax examples corrected throughout. Several examples used outdated or wrong syntax:
[table]for table calls (should be[@table]),!set name=...for variables (should beSet: name=...),N[table]for repetition (should be[@N table]), and[when expr][do …][else …][end]for conditionals (should be[when]expr[do]…[else]…[end]). All rewritten to match what the parser actually accepts.
-
Generator-root folder helpers in settings. When the Generator root path is set but the folder doesn't exist yet, a Create folder button appears under it. Once the folder exists, an Add examples button writes five bundled example
.iptfiles plus a README into it. Makes first-time setup a two-click experience instead of "open file explorer, create folder, come back, type path". -
Five bundled example generators (
01-greetings.ipt→05-treasure-dictionary.ipt) demonstrating the language features in progressive order — basics, sub-table composition, variables/prompts/dice/inline tables, lookup tables, and dictionary tables with conditionals. Each is heavily commented; they're meant as both runnable examples and a learning resource. -
New reference-guide sections for Lookup tables, Dictionary tables, Prompts, Variable arithmetic, and Getting started — each with multiple worked examples. Many additional examples added throughout existing sections.
- Added 12 regression tests in
__tests__/integration/ipp3-compat.test.tscovering each IPP3 compatibility fix, plus 6 in__tests__/integration/bundled-examples.test.tsverifying every shipped example produces output. Total: 975 tests across 39 suites, all green.
- Dictionary keys with spaces or other punctuation now work in
api.roll/rollUnscopedviadictKey. 1.0.11 built[#<key> <Table>]expressions internally; that form whitespace- splits, so a key like"Knight Bachelor"was misparsed as keyKnightagainst a non-existent tableBachelor <Table>. The API now looks the entry up directly via a newEvaluator.runByKeymethod, passing the key verbatim. Hyphenated, punctuated, and embedded-quote keys all resolve.
- Quoted-key syntax for IPP3 dictionary lookups. In a
.iptfile, write[#"key with spaces" Table]to look up a dictionary entry whose key isn't a single bareword. Embedded double-quotes can be escaped:[#"a \"b\" c" Table]. Unquoted keys ([#Plain Table],[#Master-Adept Table],[#{$var} Table]) continue to work exactly as before — the quoted form is additive, not a syntax change. Reported by claudermilk while building an NPC generator driven by meta-bind dropdowns. - Community generators section in settings. Two buttons: one
opens the
community-generators/folder on GitHub to browse contributions; the other opens a pre-filled GitHub issue for submitting your own. Contributions are stored in the repo and reviewed by maintainers before being added. API.mdrecipe for storing roll results in frontmatter. Documents the dataviewjs feedback loop that happens when render- time blocks write back to the same note, and shows two patterns to avoid it (seed off a stable value, or move writes out of the render path).
dictKeyoption for dictionary tables.roll()androllUnscoped()now accept adictKeyto look up an entry in aType: Dictionarytable — equivalent to the IPP3[#<key> <Table>]pick syntax. Reported: callingapi.roll()on a dictionary table silently returned an empty string because dictionaries aren't rolled randomly andpromptValuesdoesn't address dictionary keys. TherollExpression("[#<key> <Table>]")form already worked and continues to;dictKeyis the typed convenience for callers that have a key in hand (typically from frontmatter or a meta-bind input). Unknown keys return an empty string, matching IPP3's[#bogus Table]behaviour.
- macOS Unicode (NFD/NFC) filename matching. macOS filesystems
store names in Unicode NFD (decomposed); a
Use:reference typed or stored in NFC has different bytes for any accented/combining character even though it looks identical. The index and the file-source lookups now normalise both sides to NFC before comparing, so a reference matches its on-disk file regardless of composition form. (Pure-ASCII names are unaffected — NFC is a no-op there.)
- "Diagnose generator resolution" now dumps the raw folder listing.
The command prints every file in the active note's folder verbatim
(quoted, so trailing spaces or control characters are visible) and
unfiltered (so non-
.iptnames show too), and compares the raw adapter listing against what Obsidian'sgetFiles()reports for the same folder. This pinpoints cases where a file renders in Obsidian's tree/embed but aUse:reference can't match it on disk — e.g. a hidden double extension (portraits.ipt.txt), a trailing space, or an odd Unicode form.
- Legacy sub-path
Use:references now resolve. Community IPP3 files (e.g. the NBOS corpora) reference imports with Windows backslashes, lowercase, and a folder layout that doesn't match the vault — for instanceUse: nbos\names\orc.iptwhen the real file is…/Common/nbos/Names/Orc.ipt. The index fallback previously fired only for bare filenames (no slashes), so these sub-path references failed even though the target file was indexed. The fallback now also handles slashed references: it matches on the basename and, when several files share it, prefers the one whose path ends with the reference's suffix (case-insensitively). Positional resolution still takes priority, so explicit relative/rooted paths are unaffected.
- Disk-scan fallback now actually runs (root path convention). The
fallback that scans the vault on disk for
.iptfiles Obsidian hasn't indexed started its recursive walk at"/", but Obsidian's adapter uses""for the vault root — soadapter.list("/")returned nothing useful and the whole scan silently found zero files (reported asfromDiskScan: 0). The walk now starts at""and descends properly, so a sibling.iptthatgetFiles()omitted (the reported case:portraits.iptnext to its note, embeddable by Obsidian yet absent from the metadata index) is now discovered and indexed.
- "Diagnose generator resolution" now tests the adapter directly. In
addition to listing the index, it lists the active note's folder via a
live
adapter.listand attemptsadapter.readon each.iptthere — so a report shows definitively whether a file is missing from the index vs. unreadable by the adapter.
- "Diagnose generator resolution" command. Lists every
.iptfile the index currently holds (and the active note's folder-siblings) to the developer console, plus a notice with the count. Turns "why won't myUse:resolve?" from guesswork into a definitive check: if the file isn't in the list, it isn't indexed (check the generator-root setting and the.iptextension); if it is, resolution should work.
- Inline and codeblock
Use:now consult the vault index. Inlinerdm:calls andrandomnesscodeblocks resolved theirUse:directives only positionally (caller dir, generator root, vault root) via the adapter — they never used the bare-filename index the way the public API and "Rebuild generator index" do. So aUse: portraits.iptcould fail in a note even when the file was a correct sibling and the index knew exactly where it was. All three paths (inline, codeblock, API) now share the same index-backed resolution, so a bareUse:resolves consistently everywhere. This was the remaining cause of the image-embed demo's "Use: target not found" after 1.0.2–1.0.4.
- Index rebuild now reports what it found. Running "Rebuild generator index" shows an Obsidian notice summarising the result (e.g. "index rebuilt — 23 generator files, 168 tables") and logs full detail to the developer console, including a count of files discovered only by the on-disk scan — i.e. files Obsidian hadn't indexed yet. That count is a quick diagnostic: non-zero means the disk-scan fallback (1.0.3) just caught files the metadata index missed.
- Generator index now finds files Obsidian hasn't indexed. The
bare-filename resolver and the "Rebuild generator index" command both
built their index purely from Obsidian's metadata index
(
vault.getFiles()), which omits.iptfiles Obsidian hasn't registered yet — so aUse: portraits.iptcould fail to resolve, and rebuilding the index didn't help because it read from the same incomplete source. The index now also scans the vault on disk via the adapter, and reads unindexed files directly, so dropped-in.iptfiles resolve (and "Rebuild generator index" genuinely picks them up). - Builds on the 1.0.2 file-resolution fallbacks for the same root cause.
Use:target not found for files added outside Obsidian. A.iptfile dropped into the vault via Finder/Explorer could fail to resolve from aUse:directive at every location, because file lookups went only through Obsidian's metadata index (getFiles), which doesn't include files Obsidian hasn't indexed yet. File resolution now falls back to a rawadapter.list()directory scan that reads the vault contents directly, catching unindexed files (and resolving them case-insensitively). Reported on macOS withportraits.ipt.- Clearer "not found" error. When a
Use:target can't be resolved, the error now points at the most common real cause — an unindexed file — and suggests reloading Obsidian or running "Rebuild generator index", instead of implying the path is wrong.
First stable release. Full implementation and documentation of the
public JavaScript API; the complete API surface (version 1.0.0) is
implemented, covered by 64 dedicated tests, and documented in
API.md. Ships with an expanded PF2e settlement generator
library.
This release marks the plugin as stable for general use. The API surface is committed: breaking changes to it will bump the major version.
Thanks to @pjjelly17, whose PR #1 proposed the public JS API that this release builds on and documents.
rollUnscoped(tableName, opts?)— roll a table found anywhere in the vault, ignoring note scope. Searches every.iptfile (under the generator root, if configured), loads the defining file plus its fullUse:graph, and rolls. This is the method to use for note generation and automation, where no note scope is wired up. Acceptsseed,promptValues, andfilePath(to disambiguate name collisions).- Vault index — basename and table-name index over the generator
library, powering bare-filename
Use:resolution and fasterrollUnscoped. Invalidates on vault create/delete/r