x <- 1:10
-mean(x = 1:10)x <- 1:10
+mean(x = 1:10)[1] 5.5
diff --git a/README.md b/README.md index 4a6dc2c..99082be 100644 --- a/README.md +++ b/README.md @@ -95,12 +95,6 @@ Let's flourish this text, that's what I mean. ::: ``` -### Bugs to fix - -* Right now, I believe it's not sanitized against rx special characters; e.g. if you want to flourish the literal character "*" that will not work. - -* In HTML, some special characters appear different, e.g. `<` appears as `<`. That means that choosing `x <- 1:10` as a flourish target won't work, because the `<` won't get matched. - ### Things you might want but we don't plan to do * Functionality for pdf or docx. This extension is written in JavaScript for HTML; any other doc formats would require a total rebuild from the ground up. diff --git a/_extensions/flourish/flourish.js b/_extensions/flourish/flourish.js index b2812f7..2133b52 100644 --- a/_extensions/flourish/flourish.js +++ b/_extensions/flourish/flourish.js @@ -18,7 +18,11 @@ document.addEventListener("DOMContentLoaded", () => { // Find only the code chunks you care about const sourceEls = Array.from(cell.querySelectorAll('code')) - .filter(el => !el.closest('.cell-output') && !el.closest('.cell-output-stdout')); + .filter(el => + !el.closest('.cell-output') && + !el.closest('.cell-output-stdout') && + el.dataset.flourished !== 'true' // idempotent per element + ); // For each code chunk, add flourishes for (const el of sourceEls) { @@ -45,14 +49,24 @@ document.addEventListener("DOMContentLoaded", () => { } // Apply flourishes with the appropriate class name - content = injectFlourishes(content, new RegExp(pattern.regex.source, 'g'), className, pattern.mask); + content = injectFlourishes(content, pattern.regex, className, pattern.mask); } el.innerHTML = content; + el.dataset.flourished = 'true'; } } }); +// Hardcode swaps for sanitized HTML +function htmlEntityEscapeForSearch(s) { + return s + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/&/g, '&') + .replace(//g, '>'); +} + // Helper function for parsing YAML function parseDataFlourish(flourishAttr) { try { @@ -70,37 +84,32 @@ function parseDataFlourish(flourishAttr) { // pull style out and collect patterns let style = entry.style || 'default'; let mask = entry.mask || false; - let flags = key === 'target-rx' ? 'g' : undefined; + let flags = entry.flags || 'g'; const pats = []; for (const it of items) { if (typeof it === 'string') { - pats.push(it); - } - else if (it && it.style) { + pats.push(key === 'target' ? htmlEntityEscapeForSearch(it) : it); + } else if (it && it.style) { style = it.style; - } - else if (it && it.source) { - pats.push(it.source); + } else if (it && it.source) { + pats.push(key === 'target' ? htmlEntityEscapeForSearch(it.source) : it.source); if (it.flags) flags = it.flags; - } - else if (it && it.mask) { + } else if (it && it.mask) { mask = it.mask; } } if (pats.length) { - const re = new RegExp( - pats.map(p => `(${p})`).join('|'), - flags - ); - result.push({ type: key, regex: re, style: style, mask: mask }); + const pattern = key === 'target' ? pats.join('|') + : pats.map(p => `(${p})`).join('|'); + const re = new RegExp(pattern, flags); + result.push({ type: key, regex: re, style, mask }); } } } return result; - } - catch { + } catch { return null; } } @@ -110,12 +119,13 @@ function addStyle(document, className = "flr-default", styleText = `background-color: yellow; display: inline; color: inherit;`) { + // Deduplicate styles by class selector + const selector = `.${className}`; + const exists = Array.from(document.querySelectorAll('style')) + .some(s => s.textContent && s.textContent.includes(selector)); + if (exists) return; const styleSheet = document.createElement("style"); - styleSheet.textContent = ` - .${className} { - ${styleText} - } -`; + styleSheet.textContent = `${selector} { ${styleText} }`; document.head.appendChild(styleSheet); } diff --git a/example.html b/example.html index d430d18..067e026 100644 --- a/example.html +++ b/example.html @@ -2,7 +2,7 @@
- + @@ -65,8 +65,9 @@ * Licensed MIT © Zeno Rocha */ !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return b}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),r=n.n(e);function c(t){try{return document.execCommand(t)}catch(t){return}}var a=function(t){t=r()(t);return c("cut"),t};function o(t,e){var n,o,t=(n=t,o="rtl"===document.documentElement.getAttribute("dir"),(t=document.createElement("textarea")).style.fontSize="12pt",t.style.border="0",t.style.padding="0",t.style.margin="0",t.style.position="absolute",t.style[o?"right":"left"]="-9999px",o=window.pageYOffset||document.documentElement.scrollTop,t.style.top="".concat(o,"px"),t.setAttribute("readonly",""),t.value=n,t);return e.container.appendChild(t),e=r()(t),c("copy"),t.remove(),e}var f=function(t){var e=1flourishBy default, ordinary yellow highlighting is used:
-#| flourish:
-#| - target-rx: "[0-9]*"#| flourish:
+#| - target-rx: "[0-9]*"x <- 1:10
-mean(x = 1:10)x <- 1:10
+mean(x = 1:10)[1] 5.5
It is also possible to specify custom formatting with the style tag. Here you must provide the exact text that would appear in a CSS class definition.
#| flourish:
+#| flourish:
#| - target:
#| - "mean"
#| - style:
-#| - "background-color: #FFC0CB;"
+#| - "background-color: #FFC0CB;"x <- 1:10
-mean(x = 1:10)x <- 1:10
+mean(x = 1:10)[1] 5.5
flourish
Blanking Code Using flourish
One fun way to use this is to blank out code pieces for student exercises.
-#| flourish:
+#| flourish:
#| - target:
#| - "mean"
#| - style:
-#| - "color: transparent;"
+#| - "color: transparent;"
-x <- 1:10
-mean(x = 1:10)
+x <- 1:10
+mean(x = 1:10)
[1] 5.5
Note that blanking out pieces with style means the answers are still visible in the underlying HTML. To fully replace the answers with placeholder space, use mask:
-#| flourish:
+#| flourish:
#| - target:
#| - "mean"
#| - mask: true
-#| - style: "text-decoration: underline;"
+#| - style: "text-decoration: underline;"
-x <- 1:10
-mean(x = 1:10)
+x <- 1:10
+mean(x = 1:10)
[1] 5.5
@@ -2512,30 +2551,39 @@ Blanking Code
Comprehensive Example
This example seeks to serve as a reference for all your flourish needs.
-#| flourish:
-#| - target: "mean"
-#| - target:
-#| - "sd"
-#| - mask: true
-#| - style: "text-decoration: underline;"
-#| - target-rx: "[0-9]*"
-#| - target:
-#| - "x"
-#| - style: "font-weight: bold;"
+#| flourish:
+#| - target:
+#| - "*"
+#| - "<-"
+#| - "|>"
+#| - target:
+#| - "sd"
+#| - mask: true
+#| - style: "text-decoration: underline;"
+#| - target-rx: "[0-9]*"
#| - target:
-#| - "set.seed"
-#| - "rnorm"
-#| - style:
-#| - "font-style: italic;"
-#| - "background-color: #468e5d;"
-
-set.seed(0)
+#| - "mean"
+#| - "x"
+#| - style: "font-weight: bold;"
+#| - target:
+#| - "set.seed"
+#| - "rnorm"
+#| - style:
+#| - "font-style: italic;"
+#| - "background-color: #468e5d;"
+
+set.seed(0)
x <- rnorm(10)
-mean(x)
+x * 10 |> median()
+
+ [1] 12.62954285 -3.26233361 13.29799263 12.72429321 4.14641434
+ [6] -15.39950042 -9.28567035 -2.94720447 -0.05767173 24.04653389
+
+mean(x)
[1] 0.358924
-sd(x)
+sd(x)
[1] 1.205336
@@ -2544,18 +2592,18 @@ Comprehensive Exampl
Fun With CSS
Note that flourish admits (almost) any valid CSS, which allows us to do stuff like this:
-#| flourish:
-#| - target:
-#| - "wackyyy"
-#| - style:
-#| - "background-color: #bd8c51;"
-#| - "text-decoration: line-through;"
-#| - "-webkit-text-stroke: 1px black;"
-#| - "filter: blur(1px);"
-#| - "font-family: 'Brush Script MT', cursive;"
+#| flourish:
+#| - target:
+#| - "wackyyy"
+#| - style:
+#| - "background-color: #bd8c51;"
+#| - "text-decoration: line-through;"
+#| - "-webkit-text-stroke: 1px black;"
+#| - "filter: blur(1px);"
+#| - "font-family: 'Brush Script MT', cursive;"
-wackyyy <- 1:10
-mean(wackyyy)
+wackyyy <- 1:10
+mean(wackyyy)
[1] 5.5
@@ -2616,13 +2664,14 @@ Fun With CSS
e.clearSelection();
}
const getTextToCopy = function(trigger) {
- const codeEl = trigger.previousElementSibling.cloneNode(true);
- for (const childEl of codeEl.children) {
- if (isCodeAnnotation(childEl)) {
- childEl.remove();
- }
+ const outerScaffold = trigger.parentElement.cloneNode(true);
+ const codeEl = outerScaffold.querySelector('code');
+ for (const childEl of codeEl.children) {
+ if (isCodeAnnotation(childEl)) {
+ childEl.remove();
}
- return codeEl.innerText;
+ }
+ return codeEl.innerText;
}
const clipboard = new window.ClipboardJS('.code-copy-button:not([data-in-quarto-modal])', {
text: getTextToCopy
diff --git a/example.qmd b/example.qmd
index 7e7d2a1..c8516e4 100644
--- a/example.qmd
+++ b/example.qmd
@@ -100,41 +100,50 @@ This example seeks to serve as a reference for all your `flourish` needs.
```{.yaml}
#| flourish:
-#| - target: "mean"
+#| - target:
+#| - "*"
+#| - "<-"
+#| - "|>"
#| - target:
#| - "sd"
#| - mask: true
#| - style: "text-decoration: underline;"
#| - target-rx: "[0-9]*"
#| - target:
-#| - "x"
-#| - style: "font-weight: bold;"
+#| - "mean"
+#| - "x"
+#| - style: "font-weight: bold;"
#| - target:
-#| - "set.seed"
-#| - "rnorm"
-#| - style:
-#| - "font-style: italic;"
-#| - "background-color: #468e5d;"
+#| - "set.seed"
+#| - "rnorm"
+#| - style:
+#| - "font-style: italic;"
+#| - "background-color: #468e5d;"
```
```{r}
#| flourish:
-#| - target: "mean"
+#| - target:
+#| - "*"
+#| - "<-"
+#| - "|>"
#| - target:
#| - "sd"
#| - mask: true
#| - style: "text-decoration: underline;"
#| - target-rx: "[0-9]*"
#| - target:
-#| - "x"
-#| - style: "font-weight: bold;"
+#| - "mean"
+#| - "x"
+#| - style: "font-weight: bold;"
#| - target:
-#| - "set.seed"
-#| - "rnorm"
-#| - style:
-#| - "font-style: italic;"
-#| - "background-color: #468e5d;"
+#| - "set.seed"
+#| - "rnorm"
+#| - style:
+#| - "font-style: italic;"
+#| - "background-color: #468e5d;"
set.seed(0)
x <- rnorm(10)
+x * 10 |> median()
mean(x)
sd(x)
```