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=1 - + + @@ -2457,24 +2496,24 @@

Flourish Example

How to flourish

By 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
@@ -2483,27 +2522,27 @@

How to 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) ```