From 9ae06fd31ead0a9cae31fbf95d3ffa179c333d61 Mon Sep 17 00:00:00 2001 From: valentine195 <38669521+valentine195@users.noreply.github.com> Date: Mon, 19 Jul 2021 15:09:50 -0400 Subject: [PATCH] 5.1.0 - Fixes #9 - can now do random blocks by tag - Can now specify section type to return using `dice: [[Note]]|type` or `dice: #tag|type` - Can now roll multiple sections of a note --- README.md | 73 +++- manifest.json | 2 +- package.json | 2 +- src/constants.ts | 6 + src/main.css | 15 + src/main.ts | 886 ++++++++++++++++++++++++++------------------- src/parser.ts | 4 +- src/roller.ts | 117 ++++-- src/settings.ts | 29 ++ src/types/index.ts | 11 +- src/util.ts | 4 +- versions.json | 3 +- 12 files changed, 741 insertions(+), 411 deletions(-) create mode 100644 src/constants.ts create mode 100644 src/settings.ts diff --git a/README.md b/README.md index 006e8e4..d4d7fcb 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,77 @@ There is full order-of-operations support, so it can even nested into parenthese ### Random Blocks -The Dice Roller can be given a link to a note, and it will return a random block from the note. +The Dice Roller can be given a link to a note or a tag, and it will return a random block from the note/notes. This feature is still under development and may not work as expected. Usage: -`` `dice: [[Note]]` `` +| Example | Result | +| ----------------------------------------- | ----------------------------------------------- | +| `` `dice: [[Note]]` `` | Returns a single random block from `Note` | +| `` `dice: 3d[[Note]]` `` | Returns 3 random blocks from `Note` | + +#### Tags + +The Dice Roller can also be told to return results from a tag if the [Dataview](https://github.com/blacksmithgu/obsidian-dataview) plugin is installed. + +By default, this will return 1 result from _every_ file that has the tag. You can change this behavior in settings, or by using the following optional `+` and `-` parameters as shown below. + +If results from multiple files are returned, the result **from that file** can be re-rolled by clicking on the block. Clicking on the container or the dice icon will re-roll **all returned results**. + +| Example | Result | +| --------------------- | -------------------------------------------------------------------------------------------------------- | +| `` `dice: #tag` `` | Return a single random block from notes with `#tag`, behavior depends on settings (default to **every**) | +| `` `dice: #tag\|-` `` | Return a single random block from **a single, random note** with `#tag` | +| `` `dice: #tag\|+` `` | Return a single random block from **every note** with `#tag` | + +#### Block Types + +Obsidian has several "types" of blocks. Currently, the default behavior of the plugin is to filter out **thematicBreak** and **yaml** from the returned results. + +To return a specific block type, you may append `|` to the end of any block roller. + +Usage: + +| Example | Result | +| -------------------------------------------- | ------------------------------------------------------------------------------ | +| `` `dice: [[Note]]\|paragraph` `` | Return `paragraph` blocks | +| `` `dice: #tag\|paragraph,header,yaml` `` | Return `paragraph`, `header`, and `yaml` blocks | +| `` `dice: #tag\|-\|paragraph,header,yaml` `` | Return `paragraph`, `header`, and `yaml` blocks from a **single, random note** | + +I do not have any control over what Obsidian consider's each block (for instance, images may be returned as `paragraph`). + +I _believe_ that this is a list of block types defined in Obsidian, but use this with a grain of salt. + +| Type | +| -------------------- | +| `blockquote` | +| `break` | +| `code` | +| `delete` | +| `emphasis` | +| `footnoteReference` | +| `footnote` | +| `heading` | +| `html` | +| `imageReference` | +| `image` | +| `inlineCode` | +| `linkReference` | +| `link` | +| `listItem` | +| `list` | +| `paragraph` | +| `root` | +| `strong` | +| `table` | +| `text` | +| `thematicBreak` | +| `toml` | +| `yaml` | +| `definition` | +| `footnoteDefinition` | + ### Random Tables The Dice Roller may also be given a link to a table in a note, which it will read and return a random result from the table. @@ -57,8 +122,6 @@ To return multiple elements, use: Once in preview mode, you may Ctrl - click on the result to open the block reference in a new pane. -**Random tables cannot be used with modifiers. Any modifiers supplied will be ignored.** - #### Multiple Headers If a table provided to the plugin has multiple headers, the plugin will return the entire row unless you specify the header to use: @@ -97,6 +160,8 @@ Use `` `dice: 1dS` `` to roll a Fantasy AGE stunt dice. The result will show the The parser supports several modifiers. If a die has been modified, it will display _how_ it has been modified in the tooltip. +**Modifiers are only supported on basic number dice.** + If a modifier has a parameter, it will default to 1 if not provided. | Modifier | Syntax | Description | diff --git a/manifest.json b/manifest.json index 44178d3..f37b9ed 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-dice-roller", "name": "Dice Roller", - "version": "5.0.0", + "version": "5.1.0", "minAppVersion": "0.12.0", "description": "Inline dice rolling for Obsidian.md", "author": "Jeremy Valentine", diff --git a/package.json b/package.json index 35dbea4..a20cb29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-dice-roller", - "version": "5.0.0", + "version": "5.1.0", "description": "Inline dice rolling for Obsidian.md", "main": "main.js", "scripts": { diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..e14debd --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,6 @@ +export const TAG_REGEX = + /(?:(\d+)[Dd])?(#[\p{Letter}\p{Emoji_Presentation}\w/-]+)(?:\|([\+-]))?(?:\|([^\+-]+))?/u; +export const TABLE_REGEX = + /(?:(\d+)[Dd])?\[\[([\s\S]+?)#?\^([\s\S]+?)\]\]\|?([\s\S]+)?/; +export const SECTION_REGEX = /(?:(\d+)[Dd])?\[\[([\s\S]+)\]\]\|?([\s\S]+)?/; +export const MATH_REGEX = /[\(\^\+\-\*\/\)]/; diff --git a/src/main.css b/src/main.css index aff3d25..62fd82e 100644 --- a/src/main.css +++ b/src/main.css @@ -16,6 +16,11 @@ font-size: inherit; margin-left: 0.25em; } + +.dice-roller.has-embed .dice-roller-button { + padding-top: 0.5rem; +} + .dice.tooltip { max-width: unset !important; } @@ -23,6 +28,16 @@ display: flex; padding-right: 0.25rem; } + +.dice-no-results { + font-style: italic; + text-align: center; +} + .dice-roller.has-embed .internal-embed { width: 100%; } + +.dice-roller.has-embed .dice-file-name { + font-style: italic; +} diff --git a/src/main.ts b/src/main.ts index 83f383c..de9278d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,10 +13,12 @@ import { faDice } from "@fortawesome/free-solid-svg-icons"; import { icon } from "@fortawesome/fontawesome-svg-core"; import "./main.css"; -import { DiceRoll, LinkRoll, SectionRoller, StuntRoll } from "./roller"; +import { DiceRoll, TableRoll, SectionRoller, StuntRoll } from "./roller"; import { Parser } from "./parser"; -import { IConditional, ILexeme } from "src/types"; +import { Conditional, Lexeme } from "src/types"; import { extract } from "./util"; +import { MATH_REGEX, SECTION_REGEX, TABLE_REGEX, TAG_REGEX } from "./constants"; +import SettingTab from "./settings"; String.prototype.matchAll = String.prototype.matchAll || @@ -28,13 +30,45 @@ String.prototype.matchAll = yield match; } }; +//expose dataview plugin for tags +declare module "obsidian" { + interface App { + plugins: { + plugins: { + dataview: { + index: { + tags: { + invMap: Map>; + map: Map>; + }; + etags: { + invMap: Map>; + map: Map>; + }; + }; + }; + }; + }; + } +} export default class DiceRoller extends Plugin { lexer: lexer; parser: Parser; + returnAllTags: boolean; async onload() { console.log("DiceRoller plugin loaded"); + const data = Object.assign( + { + returnAllTags: true + }, + this.loadData() + ); + this.returnAllTags = data.returnAllTags ?? true; + + this.addSettingTab(new SettingTab(this.app, this)); + const ICON_DEFINITION = Symbol("dice-roller-icon").toString(); const ICON_SVG = icon(faDice).html[0]; @@ -52,166 +86,66 @@ export default class DiceRoller extends Plugin { /^dice:\s*([\s\S]+)\s*?/ ); - let { result, text, link, render } = + let { text, link, renderMap, tableMap, type } = await this.parseDice(content); let container = createDiv().createDiv({ cls: "dice-roller", attr: { "aria-label": `${content}\n${text}`, - "aria-label-position": "top" + "aria-label-position": "top", + "data-dice": content } }); - container.setAttr("data-dice", content); - - let resultEl: HTMLElement; - if (link && typeof result === "string") { - resultEl = container.createDiv(); - const split = result.split( - /(\[\[(?:[\s\S]+?)\]\])/ - ); - for (let str of split) { - if (/\[\[(?:[\s\S]+?)\]\]/.test(str)) { - //link; - const [, match] = - str.match(/\[\[([\s\S]+?)\]\]/); - const internal = resultEl.createEl("a", { - cls: "internal-link", - text: match - }); - internal.onmouseover = () => { - this.app.workspace.trigger( - "link-hover", - this, //not sure - internal, //targetEl - match - .replace("^", "#^") - .split("|") - .shift(), //linkText - this.app.workspace.getActiveFile() - ?.path //source - ); - }; - internal.onclick = async ( - ev: MouseEvent - ) => { - ev.stopPropagation(); - await this.app.workspace.openLinkText( - match - .replace("^", "#^") - .split(/\|/) - .shift(), - this.app.workspace.getActiveFile() - ?.path, - ev.getModifierState("Control") - ); - }; - continue; - } - resultEl.createSpan({ text: str }); - } - } - if (render) { - container.addClass("has-embed"); - resultEl = container.createDiv("internal-embed"); - const embedded = - resultEl.createDiv("markdown-embed"); - embedded.appendChild(render); - } else { - resultEl = container.createSpan({ - text: result.toLocaleString( - navigator.language, - { - maximumFractionDigits: 2 - } - ) - }); + if (type === "render") { + container.addClasses([ + "has-embed", + "markdown-embed" + ]); } - setIcon( - container.createDiv({ cls: "dice-roller-button" }), - ICON_DEFINITION + let resultEl = container.createDiv(); + this.reroll( + null, + container, + resultEl, + content, + link, + renderMap, + tableMap, + type ); - node.replaceWith(container); + const icon = container.createDiv({ + cls: "dice-roller-button" + }); + setIcon(icon, ICON_DEFINITION); - container.addEventListener("click", async (evt) => { - if (link && evt.getModifierState("Control")) { - await this.app.workspace.openLinkText( - link.replace("^", "#^").split(/\|/).shift(), - this.app.workspace.getActiveFile()?.path, - true - ); - return; - } - ({ result, text, render } = await this.parseDice( - content - )); - - if (link && typeof result === "string") { - resultEl.empty(); - const split = result.split( - /(\[\[(?:[\s\S]+?)\]\])/ - ); + node.replaceWith(container); - for (let str of split) { - if (/\[\[(?:[\s\S]+?)\]\]/.test(str)) { - //link; - const [, match] = - str.match(/\[\[([\s\S]+?)\]\]/); - const internal = resultEl.createEl( - "a", - { - cls: "internal-link", - text: match - } - ); - internal.onmouseover = () => { - this.app.workspace.trigger( - "link-hover", - this, //not sure - internal, //targetEl - match - .replace("^", "#^") - .split("|") - .shift(), //linkText - this.app.workspace.getActiveFile() - ?.path //source - ); - }; - internal.onclick = async ( - ev: MouseEvent - ) => { - ev.stopPropagation(); - await this.app.workspace.openLinkText( - match - .replace("^", "#^") - .split(/\|/) - .shift(), - this.app.workspace.getActiveFile() - ?.path, - ev.getModifierState("Control") - ); - }; - continue; - } - resultEl.createSpan({ text: str }); - } - } else if (render) { - resultEl.empty(); - - const embedded = - resultEl.createDiv("markdown-embed"); - embedded.appendChild(render); - } else { - resultEl.setText( - result.toLocaleString(navigator.language, { - maximumFractionDigits: 2 - }) - ); - } - }); + container.onclick = (evt) => + this.reroll( + evt, + container, + resultEl, + content, + link, + renderMap, + tableMap, + type + ); + icon.onclick = (evt) => + this.reroll( + evt, + container, + resultEl, + content, + link, + renderMap, + tableMap, + type + ); } catch (e) { console.error(e); new Notice( @@ -251,6 +185,94 @@ export default class DiceRoller extends Plugin { "^": exponent }); } + async reroll( + evt: MouseEvent, + container: HTMLElement, + resultEl: HTMLElement, + content: string, + link: string, + renderMap: Map, + tableMap: TableRoll, + type: "dice" | "table" | "render" + ) { + if (link && evt && evt.getModifierState("Control")) { + await this.app.workspace.openLinkText( + link.replace("^", "#^").split(/\|/).shift(), + this.app.workspace.getActiveFile()?.path, + true + ); + return; + } + + resultEl.empty(); + if (type === "dice") { + let { result, text } = await this.parseDice(content); + container.setAttrs({ + "aria-label": `${content}\n${text}` + }); + resultEl.setText( + result.toLocaleString(navigator.language, { + maximumFractionDigits: 2 + }) + ); + } else if (type === "table") { + resultEl.empty(); + tableMap.roll(); + const split = tableMap.result.split(/(\[\[(?:[\s\S]+?)\]\])/); + + for (let str of split) { + if (/\[\[(?:[\s\S]+?)\]\]/.test(str)) { + //link; + const [, match] = str.match(/\[\[([\s\S]+?)\]\]/); + const internal = resultEl.createEl("a", { + cls: "internal-link", + text: match + }); + internal.onmouseover = () => { + this.app.workspace.trigger( + "link-hover", + this, //not sure + internal, //targetEl + match.replace("^", "#^").split("|").shift(), //linkText + this.app.workspace.getActiveFile()?.path //source + ); + }; + internal.onclick = async (ev: MouseEvent) => { + ev.stopPropagation(); + await this.app.workspace.openLinkText( + match.replace("^", "#^").split(/\|/).shift(), + this.app.workspace.getActiveFile()?.path, + ev.getModifierState("Control") + ); + }; + continue; + } + resultEl.createSpan({ text: str }); + } + } else if (type === "render") { + resultEl.empty(); + resultEl.addClass("internal-embed"); + for (let [file, elements] of Array.from(renderMap)) { + const holder = resultEl.createDiv({ + attr: { + "aria-label": file + } + }); + if (renderMap.size > 1) { + holder.createEl("h5", { + cls: "dice-file-name", + text: file + }); + } + + for (let el of elements) { + el.roll(); + el.element(holder.createDiv()); + } + } + } + } + addLexerRules() { this.lexer.addRule(/\s+/, function () { /* skip whitespace */ @@ -258,52 +280,48 @@ export default class DiceRoller extends Plugin { this.lexer.addRule(/[{}]+/, function () { /* skip brackets */ }); - this.lexer.addRule( - /[\(\^\+\-\*\/\)]/, - function (lexeme: string): ILexeme { - return { - type: "math", - data: lexeme, - original: lexeme, - conditionals: null - }; // punctuation ("^", "(", "+", "-", "*", "/", ")") - } - ); - - this.lexer.addRule( - /((\d+)[Dd])?\[\[([\s\S]+?)#?\^([\s\S]+?)\]\]\|?([\s\S]+)?/, - function (lexeme: string): ILexeme { - /* let [, link] = lexeme.match(/\d+[Dd]\[\[([\s\S]+?)\]\]/); */ + this.lexer.addRule(MATH_REGEX, function (lexeme: string): Lexeme { + return { + type: "math", + data: lexeme, + original: lexeme, + conditionals: null + }; + }); - return { - type: "link", - data: lexeme, - original: lexeme, - conditionals: null - }; - } - ); - this.lexer.addRule( - /((\d+)[Dd])?\[\[([\s\S]+?)\]\]\|?([\s\S]+)?/, - function (lexeme: string): ILexeme { - /* let [, link] = lexeme.match(/\d+[Dd]\[\[([\s\S]+?)\]\]/); */ + this.lexer.addRule(TABLE_REGEX, function (lexeme: string): Lexeme { + return { + type: "table", + data: lexeme, + original: lexeme, + conditionals: null + }; + }); + this.lexer.addRule(SECTION_REGEX, function (lexeme: string): Lexeme { + return { + type: "section", + data: lexeme, + original: lexeme, + conditionals: null + }; + }); - return { - type: "section", - data: lexeme, - original: lexeme, - conditionals: null - }; - } - ); + this.lexer.addRule(TAG_REGEX, function (lexeme: string): Lexeme { + return { + type: "tag", + data: lexeme, + original: lexeme, + conditionals: null + }; + }); this.lexer.addRule( /(\d+)([Dd]\[?(?:(-?\d+)\s?,)?\s?(-?\d+|%|F)\]?)?/, - function (lexeme: string): ILexeme { + function (lexeme: string): Lexeme { let [, dice] = lexeme.match( /(\d+(?:[Dd]?\[?(?:-?\d+\s?,)?\s?(?:-?\d+|%|F)\]?)?)/ ), - conditionals: IConditional[] = []; + conditionals: Conditional[] = []; if (/(?:(!?=|=!|>=?|<=?)(\d+))+/.test(lexeme)) { for (const [, operator, comparer] of lexeme.matchAll( /(?:(!?=|=!|>=?|<=?)(\d+))/g @@ -322,7 +340,7 @@ export default class DiceRoller extends Plugin { }; // symbols } ); - this.lexer.addRule(/1[Dd]S/, function (lexeme: string): ILexeme { + this.lexer.addRule(/1[Dd]S/, function (lexeme: string): Lexeme { const [, dice] = lexeme.match(/1[Dd]S/) ?? [, "1"]; return { type: "stunt", @@ -332,19 +350,16 @@ export default class DiceRoller extends Plugin { }; // symbols }); - this.lexer.addRule( - /kh?(?!:l)(\d*)/, - function (lexeme: string): ILexeme { - /** keep high */ - return { - type: "kh", - data: lexeme.replace(/^\D+/g, ""), - original: lexeme, - conditionals: null - }; - } - ); - this.lexer.addRule(/dl?(?!:h)\d*/, function (lexeme: string): ILexeme { + this.lexer.addRule(/kh?(?!:l)(\d*)/, function (lexeme: string): Lexeme { + /** keep high */ + return { + type: "kh", + data: lexeme.replace(/^\D+/g, ""), + original: lexeme, + conditionals: null + }; + }); + this.lexer.addRule(/dl?(?!:h)\d*/, function (lexeme: string): Lexeme { /** drop low */ return { type: "dl", @@ -354,7 +369,7 @@ export default class DiceRoller extends Plugin { }; }); - this.lexer.addRule(/kl\d*/, function (lexeme: string): ILexeme { + this.lexer.addRule(/kl\d*/, function (lexeme: string): Lexeme { /** keep low */ return { type: "kl", @@ -363,7 +378,7 @@ export default class DiceRoller extends Plugin { conditionals: null }; }); - this.lexer.addRule(/dh\d*/, function (lexeme: string): ILexeme { + this.lexer.addRule(/dh\d*/, function (lexeme: string): Lexeme { /** drop high */ return { type: "dh", @@ -374,12 +389,12 @@ export default class DiceRoller extends Plugin { }); this.lexer.addRule( /!!(i|\d+)?(?:(!?=|=!|>=?|<=?)(-?\d+))*/, - function (lexeme: string): ILexeme { + function (lexeme: string): Lexeme { /** explode and combine */ let [, data = `1`] = lexeme.match( /!!(i|\d+)?(?:(!?=|=!|>=?|<=?)(-?\d+))*/ ), - conditionals: IConditional[] = []; + conditionals: Conditional[] = []; if (/(?:(!?=|=!|>=?|<=?)(-?\d+))+/.test(lexeme)) { for (const [, operator, comparer] of lexeme.matchAll( /(?:(!?=|=!|>=?|<=?)(-?\d+))/g @@ -404,12 +419,12 @@ export default class DiceRoller extends Plugin { ); this.lexer.addRule( /!(i|\d+)?(?:(!?=|=!?|>=?|<=?)(-?\d+))*/, - function (lexeme: string): ILexeme { + function (lexeme: string): Lexeme { /** explode */ let [, data = `1`] = lexeme.match( /!(i|\d+)?(?:(!?=|=!?|>=?|<=?)(-?\d+))*/ ), - conditionals: IConditional[] = []; + conditionals: Conditional[] = []; if (/(?:(!?=|=!|>=?|<=?)(\d+))+/.test(lexeme)) { for (const [, operator, comparer] of lexeme.matchAll( /(?:(!?=|=!?|>=?|<=?)(-?\d+))/g @@ -435,12 +450,12 @@ export default class DiceRoller extends Plugin { this.lexer.addRule( /r(i|\d+)?(?:(!?=|=!|>=?|<=?)(-?\d+))*/, - function (lexeme: string): ILexeme { + function (lexeme: string): Lexeme { /** reroll */ let [, data = `1`] = lexeme.match( /r(i|\d+)?(?:(!?=|=!|>=?|<=?)(-?\d+))*/ ), - conditionals: IConditional[] = []; + conditionals: Conditional[] = []; if (/(?:(!?={1,2}|>=?|<=?)(-?\d+))+/.test(lexeme)) { for (const [, operator, comparer] of lexeme.matchAll( /(?:(!?=|=!|>=?|<=?)(-?\d+))/g @@ -481,195 +496,315 @@ export default class DiceRoller extends Plugin { result: string | number; text: string; link?: string; - render?: HTMLElement; + tableMap: TableRoll; + renderMap: Map; + type: "dice" | "table" | "render"; }> { return new Promise(async (resolve, reject) => { let stack: Array = [], diceMap: DiceRoll[] = [], stuntMap: StuntRoll, - linkMap: LinkRoll, - sectionMap: SectionRoller; + tableMap: TableRoll, + renderMap: Map = new Map(), + type: "dice" | "table" | "render" = "dice"; const parsed = this.parse(text); parse: for (const d of parsed) { - switch (d.type) { - case "+": - case "-": - case "*": - case "/": - case "^": - case "math": - const b = stack.pop(), - a = stack.pop(), - result = this.operators[d.data](a.result, b.result); - - stack.push(new DiceRoll(`${result}`)); - break; - case "kh": { - let diceInstance = diceMap[diceMap.length - 1]; - let data = d.data ? Number(d.data) : 1; - - diceInstance.keepHigh(data); - diceInstance.modifiers.push(d.original); - break; + if (d.type === "stunt") { + stuntMap = new StuntRoll(); + if (parsed.length > 1) { + new Notice(`Stunt dice cannot be used with modifiers.`); + } + break; + } else if (d.type === "table") { + type = "table"; + const [, roll = 1, link, block, header] = + d.data.match(TABLE_REGEX), + file = + await this.app.metadataCache.getFirstLinkpathDest( + link, + "" + ); + if (!file || !(file instanceof TFile)) + reject( + "Could not read file cache. Is the link correct?\n\n" + + link + ); + const cache = await this.app.metadataCache.getFileCache( + file + ); + if (!cache || !cache.blocks || !cache.blocks[block]) + reject( + "Could not read file cache. Does the block reference exist?\n\n" + + `${link} > ${block}` + ); + const data = cache.blocks[block]; + + const content = (await this.app.vault.read(file))?.slice( + data.position.start.offset, + data.position.end.offset + ); + let table = extract(content); + let opts: string[]; + + if (header && table.columns[header]) { + opts = table.columns[header]; + } else { + if (header) + reject( + `Header ${header} was not found in table ${link} > ${block}.` + ); + opts = table.rows; } - case "dl": { - let diceInstance = diceMap[diceMap.length - 1]; - let data = d.data ? Number(d.data) : 1; - data = diceInstance.results.size - data; + tableMap = new TableRoll( + Number(roll ?? 1), + opts, + d.data, + link, + block + ); - diceInstance.keepHigh(data); - diceInstance.modifiers.push(d.original); - break; + if (parsed.length > 1) { + new Notice( + `Random tables cannot be used with modifiers.` + ); } - case "kl": { - let diceInstance = diceMap[diceMap.length - 1]; - let data = d.data ? Number(d.data) : 1; - - diceInstance.keepLow(data); - diceInstance.modifiers.push(d.original); - break; + break; + } else if (d.type === "section") { + type = "render"; + const [, roll = 1, link, filter] = + d.data.match(SECTION_REGEX), + file = + await this.app.metadataCache.getFirstLinkpathDest( + link, + "" + ); + let types: string[]; + if (filter && filter.length) { + types = filter.split(","); } - case "dh": { - let diceInstance = diceMap[diceMap.length - 1]; - let data = d.data ? Number(d.data) : 1; - - data = diceInstance.results.size - data; + if (!file || !(file instanceof TFile)) + reject( + "Could not read file cache. Is the link correct?\n\n" + + link + ); + const cache = await this.app.metadataCache.getFileCache( + file + ); + if (!cache || !cache.sections || !cache.sections.length) + reject("Could not read file cache."); + const content = await this.app.vault.read(file); + const data = cache.sections + .filter(({ type }) => + types + ? types.includes(type) + : !["yaml", "thematicBreak"].includes(type) + ) + .map((cache) => { + return { + ...cache, + file: file.basename + }; + }); - diceInstance.keepLow(data); - diceInstance.modifiers.push(d.original); - break; + const roller = new SectionRoller( + Number(roll), + data, + new Map([[file.basename, content]]), + file.basename + ); + + renderMap.set(file.basename, [ + ...(renderMap.get(file.basename) ?? []), + roller + ]); + + break; + } else if (d.type === "tag") { + type = "render"; + if (!this.app.plugins.plugins.dataview) { + new Notice( + "Tags are only supported with the Dataview plugin installed." + ); + return; } - case "!": { - let diceInstance = diceMap[diceMap.length - 1]; - let data = Number(d.data) || 1; - - diceInstance.explode(data, d.conditionals); - diceInstance.modifiers.push(d.original); - - break; + const [, roll = 1, tag, collapseTrigger, filter] = + d.data.match(TAG_REGEX); + + const collapse = + collapseTrigger === "-" + ? true + : collapseTrigger === "+" + ? false + : !this.returnAllTags; + + let types: string[]; + if (filter && filter.length) { + types = filter.split(","); } - case "!!": { - let diceInstance = diceMap[diceMap.length - 1]; - let data = Number(d.data) || 1; - - diceInstance.explodeAndCombine(data, d.conditionals); - diceInstance.modifiers.push(d.original); - - break; + const files = + this.app.plugins.plugins.dataview.index.tags.invMap.get( + tag + ); + if (!files || !files.size) { + reject( + "No files found with that tag. Is the tag correct?\n\n" + + tag + ); } - case "r": { - let diceInstance = diceMap[diceMap.length - 1]; - let data = Number(d.data) || 1; - diceInstance.reroll(data, d.conditionals); - diceInstance.modifiers.push(d.original); - break; - } - case "dice": - ///const res = this.roll(d.data); - - diceMap.push(new DiceRoll(d.data)); - stack.push(diceMap[diceMap.length - 1]); - break; - case "stunt": - stuntMap = new StuntRoll(); - if (parsed.length > 1) { - new Notice( - `Stunt dice cannot be used with modifiers.` + const couldNotRead = [], + noCache = []; + for (let link of files) { + let file = + await this.app.metadataCache.getFirstLinkpathDest( + link, + "" ); - } - break parse; - case "link": - const [, roll, link, block, header] = d.data.match( - /(?:(\d+)[Dd])?\[\[([\s\S]+?)#?\^([\s\S]+?)\]\]\|?([\s\S]+)?/ - ), - file = - await this.app.metadataCache.getFirstLinkpathDest( - link, - "" - ); if (!file || !(file instanceof TFile)) - reject( - "Could not read file cache. Is the link correct?\n\n" + - link - ); + couldNotRead.push(link); const cache = await this.app.metadataCache.getFileCache( file ); - if (!cache || !cache.blocks || !cache.blocks[block]) - reject( - "Could not read file cache. Does the block reference exist?\n\n" + - `${link} > ${block}` - ); - const data = cache.blocks[block]; + if (!cache || !cache.sections || !cache.sections.length) + noCache.push(link); - const content = ( - await this.app.vault.read(file) - )?.slice( - data.position.start.offset, - data.position.end.offset - ); - let table = extract(content); - let opts: string[]; + const content = await this.app.vault.read(file); + const data = cache.sections + .filter(({ type }) => + types + ? types.includes(type) + : !["yaml", "thematicBreak"].includes(type) + ) + .map((cache) => { + return { + ...cache, + file: file.basename + }; + }); - if (header && table.columns[header]) { - opts = table.columns[header]; + if (collapse) { + let roller; + const rollers = renderMap.get("all"); + if (rollers && rollers.length) { + roller = rollers.shift(); + roller.options = [...roller.options, ...data]; + roller.content.set(file.basename, content); + } else { + roller = new SectionRoller( + Number(roll), + data, + new Map([[file.basename, content]]), + "all" + ); + } + renderMap.set("all", [ + ...(renderMap.get("all") ?? []), + roller + ]); } else { - if (header) - reject( - `Header ${header} was not found in table ${link} > ${block}.` + const roller = new SectionRoller( + Number(roll), + data, + new Map([[file.basename, content]]), + file.basename + ); + renderMap.set(file.basename, [ + ...(renderMap.get(file.basename) ?? []), + roller + ]); + } + } + + break; + } else { + switch (d.type) { + case "+": + case "-": + case "*": + case "/": + case "^": + case "math": + const b = stack.pop(), + a = stack.pop(), + result = this.operators[d.data]( + a.result, + b.result ); - opts = table.rows; + + stack.push(new DiceRoll(`${result}`)); + break; + case "kh": { + let diceInstance = diceMap[diceMap.length - 1]; + let data = d.data ? Number(d.data) : 1; + + diceInstance.keepHigh(data); + diceInstance.modifiers.push(d.original); + break; } + case "dl": { + let diceInstance = diceMap[diceMap.length - 1]; + let data = d.data ? Number(d.data) : 1; - linkMap = new LinkRoll( - Number(roll ?? 1), - opts, - d.data, - link, - block - ); + data = diceInstance.results.size - data; - if (parsed.length > 1) { - new Notice( - `Random tables cannot be used with modifiers.` - ); + diceInstance.keepHigh(data); + diceInstance.modifiers.push(d.original); + break; } - break parse; - - case "section": { - const [, roll, link] = d.data.match( - /(?:(\d+)[Dd])?\[\[([\s\S]+)\]\]/ - ), - file = - await this.app.metadataCache.getFirstLinkpathDest( - link, - "" - ); - if (!file || !(file instanceof TFile)) - reject( - "Could not read file cache. Is the link correct?\n\n" + - link + case "kl": { + let diceInstance = diceMap[diceMap.length - 1]; + let data = d.data ? Number(d.data) : 1; + + diceInstance.keepLow(data); + diceInstance.modifiers.push(d.original); + break; + } + case "dh": { + let diceInstance = diceMap[diceMap.length - 1]; + let data = d.data ? Number(d.data) : 1; + + data = diceInstance.results.size - data; + + diceInstance.keepLow(data); + diceInstance.modifiers.push(d.original); + break; + } + case "!": { + let diceInstance = diceMap[diceMap.length - 1]; + let data = Number(d.data) || 1; + + diceInstance.explode(data, d.conditionals); + diceInstance.modifiers.push(d.original); + + break; + } + case "!!": { + let diceInstance = diceMap[diceMap.length - 1]; + let data = Number(d.data) || 1; + + diceInstance.explodeAndCombine( + data, + d.conditionals ); - const cache = await this.app.metadataCache.getFileCache( - file - ); - if (!cache || !cache.sections || !cache.sections.length) - reject("Could not read file cache."); - const content = await this.app.vault.read(file); - const data = cache.sections.filter( - ({ type }) => - !["yaml", "thematicBreak"].includes(type) - ); + diceInstance.modifiers.push(d.original); - sectionMap = new SectionRoller( - Number(roll), - data, - content - ); + break; + } + case "r": { + let diceInstance = diceMap[diceMap.length - 1]; + let data = Number(d.data) || 1; - break parse; + diceInstance.reroll(data, d.conditionals); + diceInstance.modifiers.push(d.original); + break; + } + case "dice": + ///const res = this.roll(d.data); + + diceMap.push(new DiceRoll(d.data)); + stack.push(diceMap[diceMap.length - 1]); + break; } } } @@ -682,28 +817,35 @@ export default class DiceRoller extends Plugin { if (stuntMap) { text = /* text.replace(/1[Dd]S/, */ stuntMap.display; } - if (linkMap) { + if (tableMap) { text = text.replace( - linkMap.text, - `${linkMap.link} > ${linkMap.block}` + tableMap.text, + `${tableMap.link} > ${tableMap.block}` ); } + if (renderMap && renderMap.size) { + text = `Results from ${renderMap.size} file${ + renderMap.size != 1 ? "s" : "" + }`; + } resolve({ - result: sectionMap + result: renderMap.size ? null - : linkMap - ? linkMap.result + : tableMap + ? tableMap.result : stuntMap ? stuntMap.result : stack[0].result, text: text, - link: `${linkMap?.link}#^${linkMap?.block}` ?? null, - render: sectionMap ? await sectionMap.element() : null + link: `${tableMap?.link}#^${tableMap?.block}` ?? null, + type, + tableMap, + renderMap }); }); } - parse(input: string): ILexeme[] { + parse(input: string): Lexeme[] { this.lexer.setInput(input); var tokens = [], token; diff --git a/src/parser.ts b/src/parser.ts index 9e10ccb..46f4692 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,11 +1,11 @@ -import { ILexeme } from "src/types"; +import { Lexeme } from "src/types"; export class Parser { table: any; constructor(table: any) { this.table = table; } - parse(input: ILexeme[]) { + parse(input: Lexeme[]) { var length = input.length, table = this.table, output = [], diff --git a/src/roller.ts b/src/roller.ts index 2724fb3..90b5d21 100644 --- a/src/roller.ts +++ b/src/roller.ts @@ -1,5 +1,10 @@ -import { IConditional, ResultMapInterface, Roller } from "src/types"; -import { MarkdownRenderer, Notice, SectionCache, TFile } from "obsidian"; +import { + Conditional, + ResultMapInterface, + Roller, + SectionCacheWithFile +} from "src/types"; +import { MarkdownRenderer, Notice } from "obsidian"; import { _checkCondition, _getRandomBetween, _insertIntoMap } from "./util"; export class DiceRoll implements Roller { @@ -12,7 +17,7 @@ export class DiceRoll implements Roller { originalRoll: number[]; modifiersAllowed: boolean = true; static: boolean = false; - conditions: IConditional[]; + conditions: Conditional[]; get result() { if (this.static) { @@ -99,7 +104,7 @@ export class DiceRoll implements Roller { this.results.set(index, { ...previous }); }); } - reroll(times: number, conditionals: IConditional[]) { + reroll(times: number, conditionals: Conditional[]) { if (!this.modifiersAllowed) { new Notice("Modifiers are only allowed on dice rolls."); return; @@ -138,7 +143,7 @@ export class DiceRoll implements Roller { this.results.set(index, value); }); } - explodeAndCombine(times: number, conditionals: IConditional[]) { + explodeAndCombine(times: number, conditionals: Conditional[]) { if (!this.modifiersAllowed) { new Notice("Modifiers are only allowed on dice rolls."); return; @@ -176,7 +181,7 @@ export class DiceRoll implements Roller { } }); } - explode(times: number, conditionals: IConditional[]) { + explode(times: number, conditionals: Conditional[]) { if (!this.modifiersAllowed) { new Notice("Modifiers are only allowed on dice rolls."); return; @@ -269,7 +274,7 @@ export class StuntRoll implements Roller { } } -export class LinkRoll implements Roller { +export class TableRoll implements Roller { resultArray: string[]; get result() { return this.resultArray.join("|"); @@ -284,44 +289,106 @@ export class LinkRoll implements Roller { public link: string, public block: string ) { - this.resultArray = this.roll(); + this.roll(); } roll() { - return [...Array(this.rolls)].map( + return (this.resultArray = [...Array(this.rolls)].map( () => this.options[_getRandomBetween(0, this.options.length - 1)] - ); + )); } } -export class SectionRoller implements Roller { - resultArray: SectionCache[]; +export class SectionRoller implements Roller { + resultArray: SectionCacheWithFile[]; + private selected: Set = new Set(); - constructor(public rolls: number, public options: SectionCache[], private content: string) { - this.resultArray = this.roll(); + constructor( + public rolls: number = 1, + public options: SectionCacheWithFile[], + public content: Map, + public file: string + ) { + if (!rolls) this.rolls = 1; + this.roll(); } get result() { return this.resultArray[0]; } get display() { + let res = this.content + .get(this.file) + .slice( + this.result.position.start.offset, + this.result.position.end.offset + ); - let res = this.content.slice( - this.result.position.start.offset, - this.result.position.end.offset - ); + return `${res}`; + } + displayFromCache(cache: SectionCacheWithFile) { + let res = this.content + .get(cache.file) + .slice(cache.position.start.offset, cache.position.end.offset); return `${res}`; } - async element() { - const ret = createDiv(); + get remaining() { + return this.options.filter((o) => !this.selected.has(o)); + } - MarkdownRenderer.renderMarkdown(this.display, ret, '', null); + element(parent: HTMLElement) { + parent.empty(); + const holder = parent.createDiv(); - return ret; + for (let result of Array.from(this.selected)) { + const resultEl = holder.createDiv(); + if (this.content.size > 1) { + resultEl.createEl("h5", { + cls: "dice-file-name", + text: result.file + }); + } + const ret = resultEl.createDiv({ + cls: "markdown-embed" + }); + if (!result) { + ret.createDiv({ + cls: "dice-no-results", + text: "No results." + }); + + continue; + } + + const embed = ret.createDiv({ + attr: { + "aria-label": `${result.file}: ${result.type}` + } + }); + MarkdownRenderer.renderMarkdown( + this.displayFromCache(result), + embed, + "", + null + ); + } + + holder.onclick = async (evt) => { + evt.stopPropagation(); + this.roll(); + this.element(parent); + }; + + return holder; } roll() { - return [...Array(this.rolls)].map( - () => this.options[_getRandomBetween(0, this.options.length - 1)] - ); + this.selected = new Set(); + this.resultArray = [...Array(this.rolls)].map(() => { + const choice = + this.remaining[_getRandomBetween(0, this.remaining.length - 1)]; + this.selected.add(choice); + return choice; + }); + return this.resultArray; } } diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..4415fdc --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,29 @@ +import { App, PluginSettingTab, Setting } from "obsidian"; +import type DiceRoller from "./main"; + +export default class SettingTab extends PluginSettingTab { + constructor(app: App, public plugin: DiceRoller) { + super(app, plugin); + this.plugin = plugin; + } + async display(): Promise { + let { containerEl } = this; + + containerEl.empty(); + + containerEl.createEl("h2", { text: "Dice Roller Settings" }); + + new Setting(containerEl) + .setName("Roll All Files for Tags") + .setDesc("Return a result for each file when rolling tags.") + .addToggle((t) => { + t.setValue(this.plugin.returnAllTags); + t.onChange(async (v) => { + this.plugin.returnAllTags = v; + await this.plugin.saveData({ + returnAllTags: this.plugin.returnAllTags + }); + }); + }); + } +} diff --git a/src/types/index.ts b/src/types/index.ts index 94312b2..91c4b63 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,11 +1,16 @@ -export interface ILexeme { +import { SectionCache } from "obsidian"; + +export interface SectionCacheWithFile extends SectionCache { + file: string; +} +export interface Lexeme { original: string; type: string; data: string; - conditionals: IConditional[]; + conditionals: Conditional[]; } -export interface IConditional { +export interface Conditional { operator: string; comparer: number; } diff --git a/src/util.ts b/src/util.ts index 103f210..d450c81 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,4 @@ -import { IConditional, ResultInterface, ResultMapInterface } from "src/types"; +import { Conditional, ResultInterface, ResultMapInterface } from "src/types"; const MATCH = /^\|?([\s\S]+?)\|?$/; const SPLIT = /\|/; @@ -70,7 +70,7 @@ export function _getRandomBetween(min: number, max: number): number { export function _checkCondition( value: number, - conditions: IConditional[] + conditions: Conditional[] ): boolean { return conditions.every(({ operator, comparer }) => { if (Number.isNaN(value) || Number.isNaN(comparer)) { diff --git a/versions.json b/versions.json index ba77ffc..e73fcc1 100644 --- a/versions.json +++ b/versions.json @@ -3,5 +3,6 @@ "3.0.2": "0.11.0", "4.0.7": "0.12.0", "4.1.0": "0.12.0", - "5.0.0": "0.12.10" + "5.0.0": "0.12.10", + "5.1.0": "0.12.10" } \ No newline at end of file