Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed assets/javascripts/.gitkeep
Empty file.
134 changes: 134 additions & 0 deletions assets/javascripts/discourse/components/composer-valid-roll.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import { debounce } from "@ember/runloop";
import icon from "discourse/helpers/d-icon";
import loadscript from "discourse/lib/load-script";
import { cook } from "discourse/lib/text";
import { i18n } from "discourse-i18n";
/* global rpgDiceRoller */

const ROLL_SELECTOR = ".bb-rollmaster[data-notation]";
const NOTE_ATTR = "data-notation";

export default class ComposerValidRoll extends Component {
@tracked hasRolls = false;
@tracked loading = false;
@tracked errors = [];

get title() {
if (this.loading) {
return i18n("rollmaster.validator.loading");
}
if (!this.hasRolls) {
return "";
}
if (this.errors.length) {
return i18n("rollmaster.validator.error");
} else {
return i18n("rollmaster.validator.success");
}
}

/**
* @returns {string}
*/
get raw() {
return this.args.composer?.reply;
}

get mayHaveRolls() {
if (!this.raw) {
return false;
}
const str = this.raw.toLowerCase();
return str.includes("[roll]") && str.includes("[/roll]");
}

@action
async checkRolls(raw) {
this.loading = true;
const actualPreview = this.args.composer.getCookedHtml();
if (actualPreview) {
this.validateRollsFromPost(
document.querySelector("#reply-control.show-preview .d-editor-preview")
);
return;
}

const cooked = await cook(raw);
const template = document.createElement("template");
template.innerHTML = cooked;
this.validateRollsFromPost(template.content);
}

@action
asyncCheckRolls() {
debounce(this, this.checkRolls, this.raw, 1000);
}

/**
* @param {Element} post
*/
async validateRollsFromPost(post) {
await this.loadRpgDiceRoller();
/** @type NodeList */
const rollEls = post.querySelectorAll(ROLL_SELECTOR);
this.errors = [];
this.hasRolls = !!rollEls.length;
this.loading = false;

if (!this.hasRolls) {
return;
}

/** @type string[] */
rollEls.forEach((el) => {
/** @type string */
const notation = el.getAttribute(NOTE_ATTR);
const rolls = notation
.split("\n")
.map((r) => r.trim())
.filter(Boolean);
rolls.forEach((roll) => {
try {
rpgDiceRoller.Parser.parse(roll);
} catch (err) {
this.errors.push(err);
}
});
});
}

async loadRpgDiceRoller() {
await Promise.all([
loadscript("/plugins/rollmaster/vendors/math.js"),
loadscript("/plugins/rollmaster/vendors/random-js.min.js"),
]);
await loadscript("/plugins/rollmaster/vendors/rpg-dice-roller.min.js");
}

<template>
{{#if this.mayHaveRolls}}
<div
{{didInsert this.asyncCheckRolls}}
{{didUpdate this.asyncCheckRolls this.raw}}
class="rollmaster-valid-composer"
title={{this.title}}
>
{{#if this.hasRolls}}
{{icon "rollmaster-dices" class="svg-roll"}}
{{#if this.errors.length}}
{{icon "triangle-exclamation" class="roll__invalid"}}
{{/if}}
{{/if}}

{{#if this.loading}}
{{icon "spinner" class="rollmaster-spinner"}}
{{/if}}
</div>
{{/if}}
</template>
}
18 changes: 18 additions & 0 deletions assets/javascripts/discourse/initializers/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { withPluginApi } from "discourse/lib/plugin-api";
import ComposerValidRoll from "../components/composer-valid-roll";

function initializeRollmasterPreview(api) {
const siteSettings = api.container.lookup("service:site-settings");
if (!siteSettings.rollmaster_enabled) {
return;
}

api.renderInOutlet("after-d-editor", ComposerValidRoll);
}

export default {
name: "rollmaster-composer-preview",
initialize() {
withPluginApi("2.0.0", initializeRollmasterPreview);
},
};
53 changes: 53 additions & 0 deletions assets/javascripts/lib/discourse-markdown/bbcode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { i18n } from "discourse-i18n";

const ROLL_CLASS = "bb-rollmaster";
const DATA_DICE = "data-notation";

function applyRollAttrs(state, token, attrs, content) {
token.attrs = [
["class", ROLL_CLASS],
[DATA_DICE, content],
];

if (content) {
token = state.push("text", "", 0);
token.content = i18n("rollmaster.bbcode.placeholder") + content;
}
}

const blockRule = {
tag: "roll",
replace(state, tagInfo, content) {
let token = state.push("roll_open", "div", 1);

applyRollAttrs(state, token, tagInfo.attrs, content);

state.push("roll_close", "div", -1);
return true;
},
};

const inlineRule = {
tag: "roll",
replace(state, tagInfo, content) {
let token = state.push("roll_open", "span", 1);

applyRollAttrs(state, token, tagInfo.attrs, content);

state.push("roll_close", "span", -1);
return true;
},
};

export function setup(helper) {
helper.allowList(["div.bb-rollmaster", "span.bb-rollmaster"]);

helper.registerOptions((opts) => {
opts.features["rollmaster"] = true;
});

helper.registerPlugin((md) => {
md.inline.bbcode.ruler.push("inline-roll", inlineRule);
md.block.bbcode.ruler.push("block-roll", blockRule);
});
}
Empty file removed assets/stylesheets/.gitkeep
Empty file.
32 changes: 32 additions & 0 deletions assets/stylesheets/common/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.rollmaster-valid-composer {
display: block;
position: absolute;
right: 0.125em;
bottom: 0em;
padding: 0.125em;

svg.svg-roll {
--success-mild: color-mix(
in srgb,
var(--success-medium),
rgb(140, 140, 140)
);
color: var(--success-mild, --success-medium);
&:has(~ .rollmaster-spinner) {
color: var(--primary-low-mid);
}

&:has(+ .roll__invalid) {
color: var(--danger);
}
}

svg.roll__invalid {
color: var(--danger);
}
}

.rollmaster-spinner {
animation: rotate-forever 1s infinite linear;
color: var(--primary-low-mid);
}
7 changes: 6 additions & 1 deletion config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ en:
rollmaster: "Rollmaster"
js:
rollmaster:
placeholder: placeholder
bbcode:
placeholder: "Rolling: "
validator:
loading: "validating roll notation..."
success: "all roll notations valid"
error: "invalid roll notation found"
5 changes: 5 additions & 0 deletions plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

enabled_site_setting :rollmaster_enabled

register_asset "stylesheets/common/index.scss"

module ::Rollmaster
PLUGIN_NAME = "rollmaster"
end
Expand All @@ -18,6 +20,9 @@ module ::Rollmaster

after_initialize do
# Code which should run after Rails has finished booting

register_svg_icon "rollmaster-dices"

# I don't think this is needed, but it doesn't hurt to be safe
::Rollmaster::DiceEngine.reset_context

Expand Down
8 changes: 8 additions & 0 deletions svg-icons/sprites.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file removed test/javascripts/.gitkeep
Empty file.
59 changes: 59 additions & 0 deletions test/javascripts/acceptance/composer-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { click, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
import { i18n } from "discourse-i18n";

acceptance("Rollmaster - composer", function (needs) {
needs.user();
needs.site({ can_tag_topics: true });
needs.settings({
bbcode_enabled: false, // overlap with other plugins. for local dev
rollmaster_enabled: true,
allow_uncategorized_topics: true,
});

test("bbcode [roll] is rendered", async function (assert) {
await visit("/");
await click("#create-topic");

await fillIn(".d-editor-input", "hello world");

assert.dom(".d-editor-preview").hasText("hello world");

await fillIn(".d-editor-input", "[roll]test[/roll]");

assert
.dom(".d-editor-preview .bb-rollmaster")
.hasAttribute("data-notation", "test");

await fillIn(".d-editor-input", "[roll]2d20[/roll]");

assert
.dom(".d-editor-preview .bb-rollmaster")
.hasAttribute("data-notation", "2d20");
});

test("[roll] notation is validated", async function (assert) {
await visit("/");
await click("#create-topic");

await fillIn(".d-editor-input", "hello world");

assert.dom(".d-editor-preview").hasText("hello world");
assert.dom(".rollmaster-valid-composer").doesNotExist();

await fillIn(".d-editor-input", "[roll]2d20[/roll]");

assert.dom(".rollmaster-valid-composer").exists();
assert
.dom(".rollmaster-valid-composer")
.hasAttribute("title", i18n("rollmaster.validator.success"));

await fillIn(".d-editor-input", "[roll]junk[/roll]");
assert.dom(".rollmaster-valid-composer").exists();
assert
.dom(".rollmaster-valid-composer")
.hasAttribute("title", i18n("rollmaster.validator.error"));
assert.dom(".rollmaster-valid-composer .roll__invalid").exists();
});
});