From b73433cdf90b32eb0ce7915f147a9201064481e7 Mon Sep 17 00:00:00 2001 From: Liam Bigelow <40188355+bglw@users.noreply.github.com> Date: Sun, 10 Sep 2023 20:58:00 +1200 Subject: [PATCH 01/18] Wire up a placeholder highlighting script --- pagefind/features/highlighting/base.feature | 18 ++++++++++++++++++ pagefind/src/output/mod.rs | 12 ++++++++++++ pagefind_web_js/build.js | 12 ++++++++++++ pagefind_web_js/lib/highlight.ts | 1 + 4 files changed, 43 insertions(+) create mode 100644 pagefind/features/highlighting/base.feature create mode 100644 pagefind_web_js/lib/highlight.ts diff --git a/pagefind/features/highlighting/base.feature b/pagefind/features/highlighting/base.feature new file mode 100644 index 00000000..8919d10f --- /dev/null +++ b/pagefind/features/highlighting/base.feature @@ -0,0 +1,18 @@ +Feature: Highlighting Tests + Background: + Given I have the environment variables: + | PAGEFIND_SITE | public | + + Scenario: Highlight script is loaded + Given I have a "public/page/index.html" file with the body: + """ +

Nothing

+ + """ + When I run my program + Then I should see "Running Pagefind" in stdout + Then I should see the file "public/pagefind/pagefind-highlight.js" + When I serve the "public" directory + When I load "/page/" + Then There should be no logs + Then The selector ".test" should contain "Hello from the highlight script" diff --git a/pagefind/src/output/mod.rs b/pagefind/src/output/mod.rs index 54d4affb..6d9e1bc8 100644 --- a/pagefind/src/output/mod.rs +++ b/pagefind/src/output/mod.rs @@ -62,6 +62,12 @@ const SEARCH_JS: &str = include_str!(concat!( env!("CARGO_PKG_VERSION"), ".js" )); +const HIGHLIGHT_JS: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/vendor/pagefind_highlight.", + env!("CARGO_PKG_VERSION"), + ".js" +)); pub struct LanguageMeta { pub page_count: usize, @@ -128,6 +134,12 @@ async fn write_common( Compress::None, write_behavior, ), + write( + outdir.join("pagefind-highlight.js"), + vec![HIGHLIGHT_JS], + Compress::None, + write_behavior, + ), write( outdir.join("pagefind-ui.js"), vec![WEB_UI_JS], diff --git a/pagefind_web_js/build.js b/pagefind_web_js/build.js index 33a7005c..99ca925b 100644 --- a/pagefind_web_js/build.js +++ b/pagefind_web_js/build.js @@ -28,6 +28,18 @@ const build = async () => { const compiledVendor = await esbuild.build(esbuildVendorOptions); console.log(`Vendor Build: `, compiledVendor); + // Coupled search vendor build + const esbuildVendorHighlightOptions = { + ...commonOpts, + entryPoints: [path.join(__dirname, 'lib/highlight.ts')], + entryNames: `pagefind_[name].${version}`, + outdir: path.join(__dirname, `../pagefind/vendor/`), + format: 'esm', + target: 'es2020' + } + const compiledVendorHighlight = await esbuild.build(esbuildVendorHighlightOptions); + console.log(`Vendor Highlight Build: `, compiledVendorHighlight); + // // CJS "main" build // const esbuildCjsOptions = { // ...commonOpts, diff --git a/pagefind_web_js/lib/highlight.ts b/pagefind_web_js/lib/highlight.ts new file mode 100644 index 00000000..65767022 --- /dev/null +++ b/pagefind_web_js/lib/highlight.ts @@ -0,0 +1 @@ +document.querySelector(".test")!.innerHTML = "Hello from the highlight script"; \ No newline at end of file From f5e6a8d678c56d428920b128a59eaa8e54e472b9 Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Sun, 10 Sep 2023 10:08:21 -0500 Subject: [PATCH 02/18] query param opt --- pagefind_ui/default/svelte/result.svelte | 295 ++++--- .../default/svelte/result_with_subs.svelte | 16 +- pagefind_ui/default/svelte/ui.svelte | 813 +++++++++--------- 3 files changed, 569 insertions(+), 555 deletions(-) diff --git a/pagefind_ui/default/svelte/result.svelte b/pagefind_ui/default/svelte/result.svelte index e1fca739..84767338 100644 --- a/pagefind_ui/default/svelte/result.svelte +++ b/pagefind_ui/default/svelte/result.svelte @@ -1,162 +1,159 @@
  • - {#if data} - {#if show_images} -
    - {#if data.meta.image} - {data.meta?.image_alt - {/if} -
    - {/if} -
    -

    - {data.meta?.title} -

    -

    {@html data.excerpt}

    - {#if meta.length} - - {/if} -
    - {:else} - {#if show_images} -
    + {#if data} + {#if show_images} +
    + {#if data.meta.image} + {data.meta?.image_alt {/if} -
    -

    - {placeholder(30)} -

    -

    - {placeholder(40)} -

    -
    +
    + {/if} +
    +

    + {@html data.meta?.title} +

    +

    {@html data.excerpt}

    + {#if meta.length} +
      + {#each meta as [metaTitle, metaValue]} +
    • + {@html metaTitle.replace(/^(\w)/, (c) => c.toLocaleUpperCase())}: {@html metaValue} +
    • + {/each} +
    + {/if} +
    + {:else} + {#if show_images} +
    {/if} +
    +

    + {placeholder(30)} +

    +

    + {placeholder(40)} +

    +
    + {/if}
  • + .pagefind-ui__result { + list-style-type: none; + display: flex; + align-items: flex-start; + gap: min(calc(40px * var(--pagefind-ui-scale)), 3%); + padding: calc(30px * var(--pagefind-ui-scale)) 0 + calc(40px * var(--pagefind-ui-scale)); + border-top: solid var(--pagefind-ui-border-width) var(--pagefind-ui-border); + } + .pagefind-ui__result:last-of-type { + border-bottom: solid var(--pagefind-ui-border-width) + var(--pagefind-ui-border); + } + .pagefind-ui__result-thumb { + width: min(30%, calc((30% - (100px * var(--pagefind-ui-scale))) * 100000)); + max-width: calc(120px * var(--pagefind-ui-scale)); + margin-top: calc(10px * var(--pagefind-ui-scale)); + aspect-ratio: var(--pagefind-ui-image-box-ratio); + position: relative; + } + .pagefind-ui__result-image { + display: block; + position: absolute; + left: 50%; + transform: translateX(-50%); + font-size: 0; + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; + border-radius: var(--pagefind-ui-image-border-radius); + } + .pagefind-ui__result-inner { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + margin-top: calc(10px * var(--pagefind-ui-scale)); + } + .pagefind-ui__result-title { + display: inline-block; + font-weight: 700; + font-size: calc(21px * var(--pagefind-ui-scale)); + margin-top: 0; + margin-bottom: 0; + } + .pagefind-ui__result-title .pagefind-ui__result-link { + color: var(--pagefind-ui-text); + text-decoration: none; + } + .pagefind-ui__result-title .pagefind-ui__result-link:hover { + text-decoration: underline; + } + .pagefind-ui__result-excerpt { + display: inline-block; + font-weight: 400; + font-size: calc(16px * var(--pagefind-ui-scale)); + margin-top: calc(4px * var(--pagefind-ui-scale)); + margin-bottom: 0; + min-width: calc(250px * var(--pagefind-ui-scale)); + } + .pagefind-ui__loading { + color: var(--pagefind-ui-text); + background-color: var(--pagefind-ui-text); + border-radius: var(--pagefind-ui-border-radius); + opacity: 0.1; + pointer-events: none; + } + .pagefind-ui__result-tags { + list-style-type: none; + padding: 0; + display: flex; + gap: calc(20px * var(--pagefind-ui-scale)); + flex-wrap: wrap; + margin-top: calc(20px * var(--pagefind-ui-scale)); + } + .pagefind-ui__result-tag { + padding: calc(4px * var(--pagefind-ui-scale)) + calc(8px * var(--pagefind-ui-scale)); + font-size: calc(14px * var(--pagefind-ui-scale)); + border-radius: var(--pagefind-ui-border-radius); + background-color: var(--pagefind-ui-tag); + } + \ No newline at end of file diff --git a/pagefind_ui/default/svelte/result_with_subs.svelte b/pagefind_ui/default/svelte/result_with_subs.svelte index 1f16885e..a22e49df 100644 --- a/pagefind_ui/default/svelte/result_with_subs.svelte +++ b/pagefind_ui/default/svelte/result_with_subs.svelte @@ -2,6 +2,8 @@ export let show_images = true; export let process_result = null; export let result = { data: async () => {} }; + // string or null + export let highlight_query_param = null; const skipMeta = ["title", "image", "image_alt", "url"]; @@ -10,6 +12,12 @@ let non_root_sub_results = []; let has_root_sub_result = false; + // make sure reactive + let href = data.meta?.url || data.url; + + if (highlight_query_param) { + href = `${href}?${highlight_query_param}`; + } const thin_sub_results = (results, limit) => { if (results.length <= limit) { return results; @@ -59,9 +67,7 @@ {/if}

    - {data.meta?.title} + {@html data.meta?.title}

    {#if has_root_sub_result}

    {@html data.excerpt}

    @@ -71,7 +77,7 @@

    {subres.title}{@html subres.title}

    {@html subres.excerpt}

    @@ -82,7 +88,7 @@ diff --git a/pagefind_ui/default/svelte/ui.svelte b/pagefind_ui/default/svelte/ui.svelte index b38935ba..dcb5b35a 100644 --- a/pagefind_ui/default/svelte/ui.svelte +++ b/pagefind_ui/default/svelte/ui.svelte @@ -1,438 +1,449 @@
    - From 891f384e49b96eb6a50c14486098b79802d45f6d Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Sun, 10 Sep 2023 10:08:46 -0500 Subject: [PATCH 03/18] query param opt --- pagefind_ui/default/ui-core.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pagefind_ui/default/ui-core.js b/pagefind_ui/default/ui-core.js index f2397021..e8a2c5f2 100644 --- a/pagefind_ui/default/ui-core.js +++ b/pagefind_ui/default/ui-core.js @@ -34,6 +34,8 @@ export class PagefindUI { let debounceTimeoutMs = opts.debounceTimeoutMs ?? 300; let mergeIndex = opts.mergeIndex ?? []; let translations = opts.translations ?? []; + let pagefindQueryParamName = + opts.pagefindQueryParamName ?? "pagefind-highlight"; // Remove the UI-specific config before passing it along to the Pagefind backend delete opts["element"]; From 7d129643cbb7ee47bb887a784197eda95551573c Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Sun, 10 Sep 2023 10:08:49 -0500 Subject: [PATCH 04/18] wip --- pagefind_web_js/lib/highlight.ts | 115 ++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/pagefind_web_js/lib/highlight.ts b/pagefind_web_js/lib/highlight.ts index 65767022..f2130c08 100644 --- a/pagefind_web_js/lib/highlight.ts +++ b/pagefind_web_js/lib/highlight.ts @@ -1 +1,114 @@ -document.querySelector(".test")!.innerHTML = "Hello from the highlight script"; \ No newline at end of file +// document.querySelector(".test")!.innerHTML = "Hello from the highlight script"; + +// this script should be imported on the result pages to enable highlighting + +// tbh not sure how to read this option +const pagefindQueryParamName = "pagefind-highlight"; +const highlightNodeElementName = "mark"; +const highlightNodeClassName = "pagefind__highlight"; + +// wait for the DOM to be ready +// read the query param +// find all occurrences of the query param in the DOM, respecting the data-pagefind attributes +// wrap the text in a mark with a class of pagefind__highlight + +// code from https://stackoverflow.com/a/31369978 + +function getElementsToHighlight() { + // could have more than one element with [data-pagefind-body] + // make sure it falls back correctly if no [data-pagefind-body] + // should fall back to the root selector ig + let pagefindBody = + document.querySelectorAll("[data-pagefind-body]") || document.body; +} + +function highlight(element, regex: RegExp) { + let document = element.ownerDocument; + + let nodes = [], + text = "", + node, + nodeIterator = document.createNodeIterator( + element, + NodeFilter.SHOW_TEXT, + null, + false + ); + + while ((node = nodeIterator.nextNode())) { + nodes.push({ + textNode: node, + start: text.length, + }); + text += node.nodeValue; + } + + if (!nodes.length) return; + + let match; + while ((match = regex.exec(text))) { + let matchLength = match[0].length; + + // Prevent empty matches causing infinite loops + if (!matchLength) { + regex.lastIndex++; + continue; + } + + for (let i = 0; i < nodes.length; ++i) { + node = nodes[i]; + let nodeLength = node.textNode.nodeValue.length; + + // Skip nodes before the match + if (node.start + nodeLength <= match.index) continue; + + // Break after the match + if (node.start >= match.index + matchLength) break; + + // Split the start node if required + if (node.start < match.index) { + nodes.splice(i + 1, 0, { + textNode: node.textNode.splitText(match.index - node.start), + start: match.index, + }); + continue; + } + + // Split the end node if required + if (node.start + nodeLength > match.index + matchLength) { + nodes.splice(i + 1, 0, { + textNode: node.textNode.splitText( + match.index + matchLength - node.start + ), + start: match.index + matchLength, + }); + } + + // Highlight the current node + let highlightNode = document.createElement(highlightNodeElementName); + highlightNode.className = highlightNodeClassName; + + node.textNode.parentNode.replaceChild(highlightNode, node.textNode); + highlightNode.appendChild(node.textNode); + } + } +} + +if (window) { + window.addEventListener("DOMContentLoaded", () => { + const query = new URLSearchParams(window.location.search).get( + pagefindQueryParamName + ); + if (!query) return; + + // regex to match the query param + const queryRegex = new RegExp(query, "gi"); + + highlight(getElementsToHighlight(), queryRegex); + + // add styles + document.head.appendChild( + document.createElement("style") + ).textContent = `:where(.${highlightNodeClassName}) { background-color: yellow; text-color: #ccc;}`; + }); +} From b9fb50ed16df86619441ee841324f454b5062c7b Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Sun, 10 Sep 2023 16:52:37 -0500 Subject: [PATCH 05/18] mark.js and @types/mark.js --- pagefind_web_js/package-lock.json | 30 ++++++++++++++++++++++++++++++ pagefind_web_js/package.json | 4 ++++ 2 files changed, 34 insertions(+) diff --git a/pagefind_web_js/package-lock.json b/pagefind_web_js/package-lock.json index bb856358..7eb6ff55 100644 --- a/pagefind_web_js/package-lock.json +++ b/pagefind_web_js/package-lock.json @@ -8,6 +8,10 @@ "name": "@pagefind/js", "version": "0.0.0", "license": "MIT", + "dependencies": { + "@types/mark.js": "^8.11.8", + "mark.js": "^8.11.1" + }, "devDependencies": { "ava": "^5.3.1", "esbuild": "^0.19.0", @@ -821,6 +825,27 @@ "node": ">= 8" } }, + "node_modules/@types/jquery": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.18.tgz", + "integrity": "sha512-sNm7O6LECFhHmF+3KYo6QIl2fIbjlPYa0PDgDQwfOaEJzwpK20Eub9Ke7VKkGsSJ2K0HUR50S266qYzRX4GlSw==", + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/mark.js": { + "version": "8.11.8", + "resolved": "https://registry.npmjs.org/@types/mark.js/-/mark.js-8.11.8.tgz", + "integrity": "sha512-BoWCd9ydi1hZxDfu/lF0v1hHMsNUjuxZEDJsdHlmm6GlKk4qxlLya7D3FS81QmabwFbYPpoDOh9603JESUkHbA==", + "dependencies": { + "@types/jquery": "*" + } + }, + "node_modules/@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==" + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1808,6 +1833,11 @@ "node": ">=6" } }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==" + }, "node_modules/matcher": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-5.0.0.tgz", diff --git a/pagefind_web_js/package.json b/pagefind_web_js/package.json index 5f2368c9..ae7931d7 100644 --- a/pagefind_web_js/package.json +++ b/pagefind_web_js/package.json @@ -26,5 +26,9 @@ "nodeArguments": [ "--loader=tsx" ] + }, + "dependencies": { + "@types/mark.js": "^8.11.8", + "mark.js": "^8.11.1" } } From 8458fa5b64ec5edcbefdeae56315cc5e5c179acc Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:33:41 -0500 Subject: [PATCH 06/18] highlight --- pagefind_ui/default/svelte/result.svelte | 19 +- .../default/svelte/result_with_subs.svelte | 26 ++- pagefind_web_js/lib/highlight.ts | 182 ++++++++---------- 3 files changed, 114 insertions(+), 113 deletions(-) diff --git a/pagefind_ui/default/svelte/result.svelte b/pagefind_ui/default/svelte/result.svelte index 84767338..a61d4e05 100644 --- a/pagefind_ui/default/svelte/result.svelte +++ b/pagefind_ui/default/svelte/result.svelte @@ -8,14 +8,14 @@ let data; let meta = []; - + // make sure reactive - let href = data.meta?.url || data.url; + // let href = data?.meta?.url || data.url; // make sure reactive - if (highlight_query_param) { - href = `${href}?${highlight_query_param}`; - } + // if (highlight_query_param) { + // href = `${href}?${highlight_query_param}`; + // } const load = async (r) => { data = await r.data(); @@ -44,7 +44,12 @@ {/if}

    - {@html data.meta?.title} + {@html data.meta?.title}

    {@html data.excerpt}

    {#if meta.length} @@ -156,4 +161,4 @@ border-radius: var(--pagefind-ui-border-radius); background-color: var(--pagefind-ui-tag); } - \ No newline at end of file + diff --git a/pagefind_ui/default/svelte/result_with_subs.svelte b/pagefind_ui/default/svelte/result_with_subs.svelte index a22e49df..6fb7e1ab 100644 --- a/pagefind_ui/default/svelte/result_with_subs.svelte +++ b/pagefind_ui/default/svelte/result_with_subs.svelte @@ -13,11 +13,11 @@ let has_root_sub_result = false; // make sure reactive - let href = data.meta?.url || data.url; + // let href = data?.meta?.url || data.url; - if (highlight_query_param) { - href = `${href}?${highlight_query_param}`; - } + // if (highlight_query_param) { + // href = `${href}?${highlight_query_param}`; + // } const thin_sub_results = (results, limit) => { if (results.length <= limit) { return results; @@ -34,10 +34,12 @@ const load = async (r) => { data = await r.data(); data = process_result?.(data) ?? data; - meta = Object.entries(data.meta).filter(([key]) => !skipMeta.includes(key)); + meta = Object.entries(data?.meta).filter( + ([key]) => !skipMeta.includes(key) + ); if (Array.isArray(data.sub_results)) { has_root_sub_result = - data.sub_results?.[0]?.url === (data.meta?.url || data.url); + data.sub_results?.[0]?.url === (data?.meta?.url || data?.url); if (has_root_sub_result) { non_root_sub_results = thin_sub_results(data.sub_results.slice(1), 3); } else { @@ -67,7 +69,12 @@ {/if}

    - {@html data.meta?.title} + {@html data.meta?.title}

    {#if has_root_sub_result}

    {@html data.excerpt}

    @@ -76,7 +83,10 @@ {#each non_root_sub_results as subres}

    - {@html subres.title}

    diff --git a/pagefind_web_js/lib/highlight.ts b/pagefind_web_js/lib/highlight.ts index f2130c08..ad30ce6f 100644 --- a/pagefind_web_js/lib/highlight.ts +++ b/pagefind_web_js/lib/highlight.ts @@ -1,114 +1,100 @@ -// document.querySelector(".test")!.innerHTML = "Hello from the highlight script"; - // this script should be imported on the result pages to enable highlighting -// tbh not sure how to read this option -const pagefindQueryParamName = "pagefind-highlight"; -const highlightNodeElementName = "mark"; -const highlightNodeClassName = "pagefind__highlight"; - -// wait for the DOM to be ready -// read the query param -// find all occurrences of the query param in the DOM, respecting the data-pagefind attributes -// wrap the text in a mark with a class of pagefind__highlight +import Mark from "mark.js"; -// code from https://stackoverflow.com/a/31369978 +// tbh not sure how to read this option -function getElementsToHighlight() { - // could have more than one element with [data-pagefind-body] - // make sure it falls back correctly if no [data-pagefind-body] - // should fall back to the root selector ig - let pagefindBody = - document.querySelectorAll("[data-pagefind-body]") || document.body; -} +// I think it's ok to let the user decide when to run this script +// waiting for DOMContentLoaded doesn't work if it already is loaded +// Ik I could work around, but this is simpler + +export default class PagefindHighlight { + pagefindQueryParamName: string; + // ? should this be an option? + highlightNodeElementName: string; + highlightNodeClassName: string; + markContext: string | HTMLElement | HTMLElement[] | NodeList | null; + markOptions: Mark.MarkOptions; + addStyles: boolean; + + constructor( + options = { + markContext: null, + pagefindQueryParamName: "pagefind-highlight", + // ? should this be an option? + highlightNodeElementName: "mark", + highlightNodeClassName: "pagefind__highlight", + markOptions: undefined, + addStyles: true, + } + ) { + const { + pagefindQueryParamName, + highlightNodeElementName, + highlightNodeClassName, + markContext, + markOptions, + addStyles, + } = options; + + this.pagefindQueryParamName = pagefindQueryParamName; + this.highlightNodeElementName = highlightNodeElementName || "mark"; + this.highlightNodeClassName = highlightNodeClassName; + this.addStyles = addStyles; + this.markContext = markContext; + + if (markOptions) { + this.markOptions = markOptions; + } else { + this.markOptions = { + className: this.highlightNodeClassName, + exclude: ["[data-pagefind-ignore]"], + }; + } -function highlight(element, regex: RegExp) { - let document = element.ownerDocument; + this.highlight(); + } - let nodes = [], - text = "", - node, - nodeIterator = document.createNodeIterator( - element, - NodeFilter.SHOW_TEXT, - null, - false - ); + // wait for the DOM to be ready + // read the query param + // find all occurrences of the query param in the DOM, respecting the data-pagefind attributes + // wrap the text in a mark with a class of pagefind__highlight - while ((node = nodeIterator.nextNode())) { - nodes.push({ - textNode: node, - start: text.length, - }); - text += node.nodeValue; + getHighlightParam(paramName: string): string { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get(paramName) || ""; } - if (!nodes.length) return; - - let match; - while ((match = regex.exec(text))) { - let matchLength = match[0].length; + // Inline styles might be too hard to override + addHighlightStyles(className: string) { + const styleElement = document.createElement("style"); + styleElement.innerText = `:where(.${className}) { background-color: yellow; color: black; }`; + document.head.appendChild(styleElement); + } - // Prevent empty matches causing infinite loops - if (!matchLength) { - regex.lastIndex++; - continue; + createMarkInstance() { + if (this.markContext) { + return new Mark(this.markContext); } - - for (let i = 0; i < nodes.length; ++i) { - node = nodes[i]; - let nodeLength = node.textNode.nodeValue.length; - - // Skip nodes before the match - if (node.start + nodeLength <= match.index) continue; - - // Break after the match - if (node.start >= match.index + matchLength) break; - - // Split the start node if required - if (node.start < match.index) { - nodes.splice(i + 1, 0, { - textNode: node.textNode.splitText(match.index - node.start), - start: match.index, - }); - continue; - } - - // Split the end node if required - if (node.start + nodeLength > match.index + matchLength) { - nodes.splice(i + 1, 0, { - textNode: node.textNode.splitText( - match.index + matchLength - node.start - ), - start: match.index + matchLength, - }); - } - - // Highlight the current node - let highlightNode = document.createElement(highlightNodeElementName); - highlightNode.className = highlightNodeClassName; - - node.textNode.parentNode.replaceChild(highlightNode, node.textNode); - highlightNode.appendChild(node.textNode); + const pagefindBody = document.querySelectorAll("[data-pagefind-body]"); + if (pagefindBody.length !== 0) { + return new Mark(pagefindBody); + } else { + return new Mark(document.body); } } -} -if (window) { - window.addEventListener("DOMContentLoaded", () => { - const query = new URLSearchParams(window.location.search).get( - pagefindQueryParamName - ); - if (!query) return; - - // regex to match the query param - const queryRegex = new RegExp(query, "gi"); - - highlight(getElementsToHighlight(), queryRegex); + markText(instance: Mark, text: string) { + instance.mark(text, this.markOptions); + } - // add styles - document.head.appendChild( - document.createElement("style") - ).textContent = `:where(.${highlightNodeClassName}) { background-color: yellow; text-color: #ccc;}`; - }); + highlight() { + const param = this.getHighlightParam(this.pagefindQueryParamName); + if (!param) return; + this.addStyles && this.addHighlightStyles(this.highlightNodeClassName); + const markInstance = this.createMarkInstance(); + this.markText(markInstance, param); + } } + +window.PagefindHighlight = PagefindHighlight; From 705c1328aeb66d66ece43faf14908b6c7c9414e2 Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:56:42 -0500 Subject: [PATCH 07/18] cleanup --- pagefind_ui/default/svelte/result.svelte | 11 +---------- pagefind_ui/default/svelte/result_with_subs.svelte | 8 +------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/pagefind_ui/default/svelte/result.svelte b/pagefind_ui/default/svelte/result.svelte index a61d4e05..1c895aed 100644 --- a/pagefind_ui/default/svelte/result.svelte +++ b/pagefind_ui/default/svelte/result.svelte @@ -8,15 +8,6 @@ let data; let meta = []; - - // make sure reactive - // let href = data?.meta?.url || data.url; - - // make sure reactive - // if (highlight_query_param) { - // href = `${href}?${highlight_query_param}`; - // } - const load = async (r) => { data = await r.data(); data = process_result?.(data) ?? data; @@ -46,7 +37,7 @@

    {@html data.meta?.title} diff --git a/pagefind_ui/default/svelte/result_with_subs.svelte b/pagefind_ui/default/svelte/result_with_subs.svelte index 6fb7e1ab..dbb75480 100644 --- a/pagefind_ui/default/svelte/result_with_subs.svelte +++ b/pagefind_ui/default/svelte/result_with_subs.svelte @@ -12,12 +12,6 @@ let non_root_sub_results = []; let has_root_sub_result = false; - // make sure reactive - // let href = data?.meta?.url || data.url; - - // if (highlight_query_param) { - // href = `${href}?${highlight_query_param}`; - // } const thin_sub_results = (results, limit) => { if (results.length <= limit) { return results; @@ -71,7 +65,7 @@

    {@html data.meta?.title} From b9af496d440a72755cc2a61273e6009cc8f4d6d5 Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:35:08 -0500 Subject: [PATCH 08/18] tests! --- pagefind/features/highlighting/base.feature | 18 ---- .../highlighting/highlighting_base.feature | 85 +++++++++++++++++++ pagefind/features/ui/ui_base.feature | 44 ++++++++++ 3 files changed, 129 insertions(+), 18 deletions(-) delete mode 100644 pagefind/features/highlighting/base.feature create mode 100644 pagefind/features/highlighting/highlighting_base.feature diff --git a/pagefind/features/highlighting/base.feature b/pagefind/features/highlighting/base.feature deleted file mode 100644 index 8919d10f..00000000 --- a/pagefind/features/highlighting/base.feature +++ /dev/null @@ -1,18 +0,0 @@ -Feature: Highlighting Tests - Background: - Given I have the environment variables: - | PAGEFIND_SITE | public | - - Scenario: Highlight script is loaded - Given I have a "public/page/index.html" file with the body: - """ -

    Nothing

    - - """ - When I run my program - Then I should see "Running Pagefind" in stdout - Then I should see the file "public/pagefind/pagefind-highlight.js" - When I serve the "public" directory - When I load "/page/" - Then There should be no logs - Then The selector ".test" should contain "Hello from the highlight script" diff --git a/pagefind/features/highlighting/highlighting_base.feature b/pagefind/features/highlighting/highlighting_base.feature new file mode 100644 index 00000000..8365645d --- /dev/null +++ b/pagefind/features/highlighting/highlighting_base.feature @@ -0,0 +1,85 @@ +Feature: Highlighting Tests + + Background: + Given I have the environment variables: + | PAGEFIND_SITE | public | + Given I have a "public/words/index.html" file with the body: + """ +

    Is this highlighted? It should be!

    +

    This should not be highlighted

    +

    This should not be highlighted

    + + """ + Given I have a "public/single-body/index.html" file with the body: + """ +
    +

    This should be highlighted

    +

    This should not be highlighted

    +
    +

    This should not be highlighted

    + + """ + Given I have a "public/multiple-bodies/index.html" file with the body: + """ +
    +

    This should be highlighted

    +

    This should not be highlighted

    +
    +

    This should not be highlighted

    +
    +

    This should be highlighted

    +

    This should not be highlighted

    +
    + + """ + When I run my program + Then I should see "Running Pagefind" in stdout + When I serve the "public" directory + + Scenario: Highlight script is loaded + When I load "/words/" + Then I should see the file "public/pagefind/pagefind-highlight.js" + Then There should be no logs + + Scenario: Highlight script marks correctly + When I load "/words/?pagefind-highlight=this" + Then There should be no logs + Then The selector "#has-highlight mark" should contain "this" + Then The selector "p[data-pagefind-ignore]:not(:has(span))" should contain "This should not be highlighted" + Then The selector "p[data-pagefind-ignore]:has(span)" should contain "This should not be highlighted" + When I load "/words/?pagefind-highlight=this&pagefind-highlight=should" + Then There should be no logs + Then The selector "#has-highlight mark:first-of-type" should contain "this" + Then The selector "#has-highlight mark:nth-of-type(2)" should contain "should" + When I load "/words/?pagefind-highlight=is+this" + Then There should be no logs + Then The selector "#has-highlight mark" should contain "Is this" + Then The selector "p[data-pagefind-ignore]" should contain "This should not be highlighted" + When I load "/words/?pagefind-highlight=highlighted%3F" + Then There should be no logs + Then The selector "#has-highlight mark" should contain "highlighted?" + When I load "/words/?pagefind-highlight=this+highlighted%3F" + Then There should be no logs + Then The selector "#has-highlight mark:first-of-type" should contain "this highlighted?" + + Scenario: Highlight script stays within pagefind-body + When I load "/single-body/?pagefind-highlight=this" + Then There should be no logs + Then The selector "#has-highlight mark" should contain "This" + Then The selector "p[data-pagefind-ignore]" should contain "This should not be highlighted" + Then The selector "#no-highlight" should contain "This should not be highlighted" + When I load "/multiple-bodies/?pagefind-highlight=this" + Then There should be no logs + Then The selector "#has-highlight mark" should contain "This" + Then The selector "p[data-pagefind-ignore]" should contain "This should not be highlighted" + Then The selector "#no-highlight" should contain "This should not be highlighted" + diff --git a/pagefind/features/ui/ui_base.feature b/pagefind/features/ui/ui_base.feature index 7359452f..c45bfe37 100644 --- a/pagefind/features/ui/ui_base.feature +++ b/pagefind/features/ui/ui_base.feature @@ -44,3 +44,47 @@ Feature: Base UI Tests """ Then There should be no logs Then The selector ".pagefind-ui__result-link" should contain "world" + + # in this senario I use the css attribute selector to make sure the link has the query param as the end + # if the link doesn't exist, the check will fail + # see https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors#syntax:~:text=%5Battr%24%3Dvalue%5D,by%20value. + + Scenario: Pagefind UI adds highlight query param + Given I have a "public/cat/index.html" file with the body: + """ +

    hello world

    +

    Hello world! How are you

    + """ + When I run my program + Then I should see "Running Pagefind" in stdout + Then I should see the file "public/pagefind/pagefind.js" + When I serve the "public" directory + When I load "/" + When I evaluate: + """ + async function() { + window.pui.triggerSearch("world"); + await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane + } + """ + Then There should be no logs + Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=world']" should contain "hello world" + When I evaluate: + """ + async function() { + window.pui.triggerSearch("hello world"); + await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane + } + """ + Then There should be no logs + Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=hello+world']" should contain "hello world" + When I evaluate: + """ + async function() { + window.pui.triggerSearch("hello world!"); + await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane + } + """ + Then There should be no logs + Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=hello+world%21']" should contain "hello world" + From b9b9448c1599fe11039a07214d547f99392ef36e Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:38:29 -0500 Subject: [PATCH 09/18] highlighting is working --- pagefind_ui/default/svelte/result.svelte | 2 +- .../default/svelte/result_with_subs.svelte | 4 +- pagefind_ui/default/svelte/ui.svelte | 4 +- pagefind_web_js/lib/highlight.ts | 41 +++++++++++++++---- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/pagefind_ui/default/svelte/result.svelte b/pagefind_ui/default/svelte/result.svelte index 1c895aed..86639611 100644 --- a/pagefind_ui/default/svelte/result.svelte +++ b/pagefind_ui/default/svelte/result.svelte @@ -38,7 +38,7 @@ {@html data.meta?.title}

    diff --git a/pagefind_ui/default/svelte/result_with_subs.svelte b/pagefind_ui/default/svelte/result_with_subs.svelte index dbb75480..5ee77ee9 100644 --- a/pagefind_ui/default/svelte/result_with_subs.svelte +++ b/pagefind_ui/default/svelte/result_with_subs.svelte @@ -65,8 +65,8 @@

    {@html data.meta?.title}

    diff --git a/pagefind_ui/default/svelte/ui.svelte b/pagefind_ui/default/svelte/ui.svelte index dcb5b35a..e946398d 100644 --- a/pagefind_ui/default/svelte/ui.svelte +++ b/pagefind_ui/default/svelte/ui.svelte @@ -42,7 +42,9 @@ trigger_search_term = ""; } - $: highlight_query_param = `${highlight_query_param_name}=${val}`; + $: highlight_query_param = new URLSearchParams([ + [highlight_query_param_name, val], + ]).toString(); let pagefind; let input_el, clear_el, diff --git a/pagefind_web_js/lib/highlight.ts b/pagefind_web_js/lib/highlight.ts index ad30ce6f..2576c573 100644 --- a/pagefind_web_js/lib/highlight.ts +++ b/pagefind_web_js/lib/highlight.ts @@ -8,6 +8,26 @@ import Mark from "mark.js"; // waiting for DOMContentLoaded doesn't work if it already is loaded // Ik I could work around, but this is simpler +// TODO use browser api to read query param, make sure special chars get encoded/decoded + +// the separateWordSearch of mark options treats each space separated word as a separate search +// I am not letting the user set it, because it should be handled on our side +// if pagefind ever supports exact matches, including spaces ('hello world'), then this should be passed as an entry in the pagefind-highlight query param +// see the tests for more examples + +// right now, since that isn't supported, to separateWordSearch should be false + + + type pagefindHighlightOptions = { + markContext: string | HTMLElement | HTMLElement[] | NodeList | null; + pagefindQueryParamName: string; + // ? should this be an option? + highlightNodeElementName: string; + highlightNodeClassName: string; + markOptions: Omit | undefined; + addStyles: boolean; + }; + export default class PagefindHighlight { pagefindQueryParamName: string; // ? should this be an option? @@ -17,8 +37,10 @@ export default class PagefindHighlight { markOptions: Mark.MarkOptions; addStyles: boolean; + // TODO type constructor options better + constructor( - options = { + options: pagefindHighlightOptions = { markContext: null, pagefindQueryParamName: "pagefind-highlight", // ? should this be an option? @@ -48,9 +70,10 @@ export default class PagefindHighlight { } else { this.markOptions = { className: this.highlightNodeClassName, - exclude: ["[data-pagefind-ignore]"], + exclude: ["*[data-pagefind-ignore]", "[data-pagefind-ignore] *"], }; } + this.markOptions.separateWordSearch = false; this.highlight(); } @@ -60,9 +83,11 @@ export default class PagefindHighlight { // find all occurrences of the query param in the DOM, respecting the data-pagefind attributes // wrap the text in a mark with a class of pagefind__highlight - getHighlightParam(paramName: string): string { + // TODO return array and get all params (to highlight multiple entitles (ex: 'hello world' and 'potato'))) + + getHighlightParams(paramName: string): string[] { const urlParams = new URLSearchParams(window.location.search); - return urlParams.get(paramName) || ""; + return urlParams.getAll(paramName); } // Inline styles might be too hard to override @@ -84,16 +109,16 @@ export default class PagefindHighlight { } } - markText(instance: Mark, text: string) { + markText(instance: Mark, text: string[]) { instance.mark(text, this.markOptions); } highlight() { - const param = this.getHighlightParam(this.pagefindQueryParamName); - if (!param) return; + const params = this.getHighlightParams(this.pagefindQueryParamName); + if (!params || params.length === 0) return; this.addStyles && this.addHighlightStyles(this.highlightNodeClassName); const markInstance = this.createMarkInstance(); - this.markText(markInstance, param); + this.markText(markInstance, params); } } From d9b8263fc36b1fcff589250e10e07f5c2e0f6eb9 Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:39:33 -0500 Subject: [PATCH 10/18] tests --- pagefind/features/ui/ui_base.feature | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pagefind/features/ui/ui_base.feature b/pagefind/features/ui/ui_base.feature index c45bfe37..d062d440 100644 --- a/pagefind/features/ui/ui_base.feature +++ b/pagefind/features/ui/ui_base.feature @@ -49,7 +49,7 @@ Feature: Base UI Tests # if the link doesn't exist, the check will fail # see https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors#syntax:~:text=%5Battr%24%3Dvalue%5D,by%20value. - Scenario: Pagefind UI adds highlight query param + Scenario: Pagefind UI adds highlight query params Given I have a "public/cat/index.html" file with the body: """

    hello world

    @@ -77,7 +77,7 @@ Feature: Base UI Tests } """ Then There should be no logs - Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=hello+world']" should contain "hello world" + Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=hello&pagefind-highlight=world']" should contain "hello world" When I evaluate: """ async function() { @@ -86,5 +86,5 @@ Feature: Base UI Tests } """ Then There should be no logs - Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=hello+world%21']" should contain "hello world" + Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=hello&pagefind-highlight=world%21']" should contain "hello world" From 9394fbfb435cda81f4c48086dd732656901be397 Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:53:58 -0500 Subject: [PATCH 11/18] highlight params --- pagefind_ui/default/svelte/ui.svelte | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pagefind_ui/default/svelte/ui.svelte b/pagefind_ui/default/svelte/ui.svelte index e946398d..7bbc8d66 100644 --- a/pagefind_ui/default/svelte/ui.svelte +++ b/pagefind_ui/default/svelte/ui.svelte @@ -42,9 +42,16 @@ trigger_search_term = ""; } - $: highlight_query_param = new URLSearchParams([ - [highlight_query_param_name, val], - ]).toString(); + // this could be changed to not split if the value is quoted + // ex: val = `hello world "foo bar"` highlightWords = ["hello", "world", "foo bar"] + + $: highlightWords = val.split(" "); + + $: highlight_query_param = new URLSearchParams( + highlightWords.map((word) => { + return [highlight_query_param_name, word]; + }) + ).toString(); let pagefind; let input_el, clear_el, From 97a80ed9e1bd39d869914eb92d438400bdde6964 Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Thu, 14 Sep 2023 19:55:35 -0500 Subject: [PATCH 12/18] disable highlighting --- pagefind/features/ui/ui_hooks.feature | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pagefind/features/ui/ui_hooks.feature b/pagefind/features/ui/ui_hooks.feature index f0451a05..3c645c4a 100644 --- a/pagefind/features/ui/ui_hooks.feature +++ b/pagefind/features/ui/ui_hooks.feature @@ -14,6 +14,7 @@ Feature: UI Hooks window.pui = new PagefindUI({ element: "#search", processTerm: (t) => t.replace("word", "search"), + highlightQueryParamName: null }); """ @@ -31,7 +32,7 @@ Feature: UI Hooks // TODO: Add more web test steps to humane instead of throwing js let el = document.querySelector(".pagefind-ui__result-link"); if (el.getAttribute("href") !== "/") { - throw new Error("Search term should have been normalized by processTerm"); + throw new Error(`Search term should have been normalized by processTerm. href: ${el.getAttribute("href")}`); } } """ From 6f84eaedaaaef8f7af3059ee62c8e51604bee68c Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Thu, 14 Sep 2023 19:55:54 -0500 Subject: [PATCH 13/18] make sure highlight option works --- pagefind_ui/default/svelte/ui.svelte | 9 +++++++-- pagefind_ui/default/ui-core.js | 8 ++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pagefind_ui/default/svelte/ui.svelte b/pagefind_ui/default/svelte/ui.svelte index 7bbc8d66..fc58133a 100644 --- a/pagefind_ui/default/svelte/ui.svelte +++ b/pagefind_ui/default/svelte/ui.svelte @@ -34,6 +34,7 @@ export let trigger_search_term = ""; export let translations = {}; // this is the name of the query param which is used to highlight the search terms after the user has navigated to a result page + // consider exposing the prop in the constructor in camelCase if needed export let highlight_query_param_name = "pagefind-highlight"; let val = ""; @@ -301,14 +302,18 @@ {show_images} {process_result} {result} - {highlight_query_param} + highlight_query_param={highlight_query_param_name + ? highlight_query_param + : null} /> {:else} {/if} {/each} diff --git a/pagefind_ui/default/ui-core.js b/pagefind_ui/default/ui-core.js index e8a2c5f2..3ea82deb 100644 --- a/pagefind_ui/default/ui-core.js +++ b/pagefind_ui/default/ui-core.js @@ -34,8 +34,11 @@ export class PagefindUI { let debounceTimeoutMs = opts.debounceTimeoutMs ?? 300; let mergeIndex = opts.mergeIndex ?? []; let translations = opts.translations ?? []; - let pagefindQueryParamName = - opts.pagefindQueryParamName ?? "pagefind-highlight"; + // setting the param to null should disable highlighting, hence this more complicated check + let highlightQueryParamName = "pagefind-highlight"; + if (opts.highlightQueryParamName !== undefined) { + highlightQueryParamName = opts.highlightQueryParamName; + } // Remove the UI-specific config before passing it along to the Pagefind backend delete opts["element"]; @@ -70,6 +73,7 @@ export class PagefindUI { debounce_timeout_ms: debounceTimeoutMs, merge_index: mergeIndex, translations, + highlight_query_param_name: highlightQueryParamName, pagefind_options: opts, }, }); From 4cc31a9895cc22cd1b9b75a21b332027b991947c Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Thu, 14 Sep 2023 21:01:15 -0500 Subject: [PATCH 14/18] move highlight param tests --- pagefind/features/ui/ui_base.feature | 44 ------- pagefind/features/ui/ui_highlight.feature | 143 ++++++++++++++++++++++ 2 files changed, 143 insertions(+), 44 deletions(-) create mode 100644 pagefind/features/ui/ui_highlight.feature diff --git a/pagefind/features/ui/ui_base.feature b/pagefind/features/ui/ui_base.feature index d062d440..7359452f 100644 --- a/pagefind/features/ui/ui_base.feature +++ b/pagefind/features/ui/ui_base.feature @@ -44,47 +44,3 @@ Feature: Base UI Tests """ Then There should be no logs Then The selector ".pagefind-ui__result-link" should contain "world" - - # in this senario I use the css attribute selector to make sure the link has the query param as the end - # if the link doesn't exist, the check will fail - # see https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors#syntax:~:text=%5Battr%24%3Dvalue%5D,by%20value. - - Scenario: Pagefind UI adds highlight query params - Given I have a "public/cat/index.html" file with the body: - """ -

    hello world

    -

    Hello world! How are you

    - """ - When I run my program - Then I should see "Running Pagefind" in stdout - Then I should see the file "public/pagefind/pagefind.js" - When I serve the "public" directory - When I load "/" - When I evaluate: - """ - async function() { - window.pui.triggerSearch("world"); - await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane - } - """ - Then There should be no logs - Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=world']" should contain "hello world" - When I evaluate: - """ - async function() { - window.pui.triggerSearch("hello world"); - await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane - } - """ - Then There should be no logs - Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=hello&pagefind-highlight=world']" should contain "hello world" - When I evaluate: - """ - async function() { - window.pui.triggerSearch("hello world!"); - await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane - } - """ - Then There should be no logs - Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=hello&pagefind-highlight=world%21']" should contain "hello world" - diff --git a/pagefind/features/ui/ui_highlight.feature b/pagefind/features/ui/ui_highlight.feature new file mode 100644 index 00000000..642a2fbe --- /dev/null +++ b/pagefind/features/ui/ui_highlight.feature @@ -0,0 +1,143 @@ +Feature: Base UI Tests + Background: + Given I have the environment variables: + | PAGEFIND_SITE | public | + + # in this senario I use the css attribute selector to make sure the link has the query param as the end + # if the link doesn't exist, the check will fail + # see https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors#syntax:~:text=%5Battr%24%3Dvalue%5D,by%20value. + + Scenario: Pagefind UI adds highlight query params + Given I have a "public/index.html" file with the body: + """ + + + + + """ + Given I have a "public/cat/index.html" file with the body: + """ +

    hello world

    +

    Hello world! How are you

    + """ + When I run my program + Then I should see "Running Pagefind" in stdout + Then I should see the file "public/pagefind/pagefind.js" + When I serve the "public" directory + When I load "/" + When I evaluate: + """ + async function() { + window.pui.triggerSearch("world"); + await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane + } + """ + Then There should be no logs + Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=world']" should contain "hello world" + When I evaluate: + """ + async function() { + window.pui.triggerSearch("hello world"); + await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane + } + """ + Then There should be no logs + Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=hello&pagefind-highlight=world']" should contain "hello world" + When I evaluate: + """ + async function() { + window.pui.triggerSearch("hello world!"); + await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane + } + """ + Then There should be no logs + Then The selector ".pagefind-ui__result-link[href$='?pagefind-highlight=hello&pagefind-highlight=world%21']" should contain "hello world" + + Scenario: Pagefind UI does not add highlight query params + Given I have a "public/index.html" file with the body: + """ + + + + + """ + Given I have a "public/cat/index.html" file with the body: + """ +

    hello world

    +

    Hello world! How are you

    + """ + When I run my program + Then I should see "Running Pagefind" in stdout + Then I should see the file "public/pagefind/pagefind.js" + When I serve the "public" directory + When I load "/" + When I evaluate: + """ + async function() { + window.pui.triggerSearch("world"); + await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane + } + """ + Then There should be no logs + Then The selector ".pagefind-ui__result-link[href$='/']" should contain "hello world" + When I evaluate: + """ + async function() { + window.pui.triggerSearch("hello world"); + await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane + } + """ + Then There should be no logs + Then The selector ".pagefind-ui__result-link[href$='/']" should contain "hello world" + + Scenario: Pagefind UI uses custom highlight query param name + Given I have a "public/index.html" file with the body: + """ + + + + + """ + Given I have a "public/cat/index.html" file with the body: + """ +

    hello world

    +

    Hello world! How are you

    + """ + When I run my program + Then I should see "Running Pagefind" in stdout + Then I should see the file "public/pagefind/pagefind.js" + When I serve the "public" directory + When I load "/" + When I evaluate: + """ + async function() { + window.pui.triggerSearch("world"); + await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane + } + """ + Then There should be no logs + Then The selector ".pagefind-ui__result-link[href$='?custom-param=world']" should contain "hello world" + When I evaluate: + """ + async function() { + window.pui.triggerSearch("hello world"); + await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane + } + """ + Then There should be no logs + Then The selector ".pagefind-ui__result-link[href$='?custom-param=hello&custom-param=world']" should contain "hello world" + When I evaluate: + """ + async function() { + window.pui.triggerSearch("hello world!"); + await new Promise(r => setTimeout(r, 1500)); // TODO: await el in humane + } + """ + Then There should be no logs + Then The selector ".pagefind-ui__result-link[href$='?custom-param=hello&custom-param=world%21']" should contain "hello world" From c7d2c6689cfd16e310a974d2abe0f724c323884f Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Fri, 15 Sep 2023 11:26:06 -0500 Subject: [PATCH 15/18] cleanup --- pagefind_web_js/lib/highlight.ts | 87 +++++++++++--------------------- 1 file changed, 29 insertions(+), 58 deletions(-) diff --git a/pagefind_web_js/lib/highlight.ts b/pagefind_web_js/lib/highlight.ts index 2576c573..7ad92249 100644 --- a/pagefind_web_js/lib/highlight.ts +++ b/pagefind_web_js/lib/highlight.ts @@ -1,97 +1,67 @@ // this script should be imported on the result pages to enable highlighting +// after a user clicks on a result, the linked page should have this script to enable highlighting import Mark from "mark.js"; -// tbh not sure how to read this option - -// I think it's ok to let the user decide when to run this script -// waiting for DOMContentLoaded doesn't work if it already is loaded -// Ik I could work around, but this is simpler - -// TODO use browser api to read query param, make sure special chars get encoded/decoded - // the separateWordSearch of mark options treats each space separated word as a separate search // I am not letting the user set it, because it should be handled on our side -// if pagefind ever supports exact matches, including spaces ('hello world'), then this should be passed as an entry in the pagefind-highlight query param +// if pagefind ever supports exact matches including spaces ('hello world'), then each sequence to be highlighted should be passed as an entry in the pagefind-highlight query param +// so if the search is "'hello world' lorem" then the query param should be "pagefind-highlight=hello%20world&pagefind-highlight=lorem" // see the tests for more examples - // right now, since that isn't supported, to separateWordSearch should be false - - type pagefindHighlightOptions = { - markContext: string | HTMLElement | HTMLElement[] | NodeList | null; - pagefindQueryParamName: string; - // ? should this be an option? - highlightNodeElementName: string; - highlightNodeClassName: string; - markOptions: Omit | undefined; - addStyles: boolean; - }; +type pagefindHighlightOptions = { + markContext: string | HTMLElement | HTMLElement[] | NodeList | null; + pagefindQueryParamName: string; + markOptions: Omit; + addStyles: boolean; +}; export default class PagefindHighlight { pagefindQueryParamName: string; - // ? should this be an option? - highlightNodeElementName: string; - highlightNodeClassName: string; markContext: string | HTMLElement | HTMLElement[] | NodeList | null; markOptions: Mark.MarkOptions; addStyles: boolean; - // TODO type constructor options better - constructor( options: pagefindHighlightOptions = { markContext: null, pagefindQueryParamName: "pagefind-highlight", - // ? should this be an option? - highlightNodeElementName: "mark", - highlightNodeClassName: "pagefind__highlight", - markOptions: undefined, + markOptions: { + className: "pagefind__highlight", + exclude: ["[data-pagefind-ignore]", "[data-pagefind-ignore] *"], + }, addStyles: true, } ) { - const { - pagefindQueryParamName, - highlightNodeElementName, - highlightNodeClassName, - markContext, - markOptions, - addStyles, - } = options; + const { pagefindQueryParamName, markContext, markOptions, addStyles } = + options; this.pagefindQueryParamName = pagefindQueryParamName; - this.highlightNodeElementName = highlightNodeElementName || "mark"; - this.highlightNodeClassName = highlightNodeClassName; this.addStyles = addStyles; this.markContext = markContext; - - if (markOptions) { - this.markOptions = markOptions; - } else { - this.markOptions = { - className: this.highlightNodeClassName, - exclude: ["*[data-pagefind-ignore]", "[data-pagefind-ignore] *"], - }; - } + this.markOptions = markOptions; + + // make sure these are always set + // if the user doesn't want to exclude anything, they should pass an empty array + // if the user doesn't want a className they should pass an empty string + this.markOptions.className ??= "pagefind__highlight"; + this.markOptions.exclude ??= [ + "[data-pagefind-ignore]", + "[data-pagefind-ignore] *", + ]; this.markOptions.separateWordSearch = false; - this.highlight(); } - // wait for the DOM to be ready - // read the query param - // find all occurrences of the query param in the DOM, respecting the data-pagefind attributes - // wrap the text in a mark with a class of pagefind__highlight - - // TODO return array and get all params (to highlight multiple entitles (ex: 'hello world' and 'potato'))) - getHighlightParams(paramName: string): string[] { const urlParams = new URLSearchParams(window.location.search); return urlParams.getAll(paramName); } // Inline styles might be too hard to override - addHighlightStyles(className: string) { + addHighlightStyles(className: string | undefined | null) { + if (!className) return; const styleElement = document.createElement("style"); styleElement.innerText = `:where(.${className}) { background-color: yellow; color: black; }`; document.head.appendChild(styleElement); @@ -116,7 +86,8 @@ export default class PagefindHighlight { highlight() { const params = this.getHighlightParams(this.pagefindQueryParamName); if (!params || params.length === 0) return; - this.addStyles && this.addHighlightStyles(this.highlightNodeClassName); + this.addStyles && + this.addHighlightStyles(this.markOptions.className as string); const markInstance = this.createMarkInstance(); this.markText(markInstance, params); } From 08d2ed8aa2a7531d04752570c9ea69da0f423910 Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Fri, 15 Sep 2023 11:42:36 -0500 Subject: [PATCH 16/18] options --- .../highlighting/highlighting_base.feature | 37 +++++++++++++++++++ pagefind_web_js/lib/highlight.ts | 5 ++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/pagefind/features/highlighting/highlighting_base.feature b/pagefind/features/highlighting/highlighting_base.feature index 8365645d..535770f9 100644 --- a/pagefind/features/highlighting/highlighting_base.feature +++ b/pagefind/features/highlighting/highlighting_base.feature @@ -41,6 +41,33 @@ Feature: Highlighting Tests new PagefindHighlight(); """ + Given I have a "public/options/index.html" file with the body: + """ +
    +

    This should be highlighted

    +

    This should not be highlighted

    +
    +

    This should not be highlighted

    +
    +

    This should be highlighted

    +

    This should not be highlighted

    +

    This should not be highlighted

    +
    + + """ When I run my program Then I should see "Running Pagefind" in stdout When I serve the "public" directory @@ -54,6 +81,7 @@ Feature: Highlighting Tests When I load "/words/?pagefind-highlight=this" Then There should be no logs Then The selector "#has-highlight mark" should contain "this" + Then The selector "#has-highlight mark.pagefind-highlight" should contain "this" Then The selector "p[data-pagefind-ignore]:not(:has(span))" should contain "This should not be highlighted" Then The selector "p[data-pagefind-ignore]:has(span)" should contain "This should not be highlighted" When I load "/words/?pagefind-highlight=this&pagefind-highlight=should" @@ -82,4 +110,13 @@ Feature: Highlighting Tests Then The selector "#has-highlight mark" should contain "This" Then The selector "p[data-pagefind-ignore]" should contain "This should not be highlighted" Then The selector "#no-highlight" should contain "This should not be highlighted" + + Scenario: Highlight script options work + When I load "/options/?custom-name=this" + Then There should be no logs + Then The selector "#has-highlight mark" should contain "This" + Then The selector "#has-highlight mark.custom-class" should contain "This" + Then The selector "p[data-pagefind-ignore]" should contain "This should not be highlighted" + Then The selector "p.ignore" should contain "This should not be highlighted" + Then The selector "#no-highlight" should contain "This should not be highlighted" diff --git a/pagefind_web_js/lib/highlight.ts b/pagefind_web_js/lib/highlight.ts index 7ad92249..8a54a1ab 100644 --- a/pagefind_web_js/lib/highlight.ts +++ b/pagefind_web_js/lib/highlight.ts @@ -28,7 +28,7 @@ export default class PagefindHighlight { markContext: null, pagefindQueryParamName: "pagefind-highlight", markOptions: { - className: "pagefind__highlight", + className: "pagefind-highlight", exclude: ["[data-pagefind-ignore]", "[data-pagefind-ignore] *"], }, addStyles: true, @@ -60,7 +60,8 @@ export default class PagefindHighlight { } // Inline styles might be too hard to override - addHighlightStyles(className: string | undefined | null) { + addHighlightStyles(className: string) { + // class name could be "" if (!className) return; const styleElement = document.createElement("style"); styleElement.innerText = `:where(.${className}) { background-color: yellow; color: black; }`; From aa502503ab1731dd1ba85180b4025532e2ddf613 Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Fri, 15 Sep 2023 11:52:25 -0500 Subject: [PATCH 17/18] types --- pagefind_web_js/lib/highlight.ts | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/pagefind_web_js/lib/highlight.ts b/pagefind_web_js/lib/highlight.ts index 8a54a1ab..7feeabd5 100644 --- a/pagefind_web_js/lib/highlight.ts +++ b/pagefind_web_js/lib/highlight.ts @@ -11,10 +11,10 @@ import Mark from "mark.js"; // right now, since that isn't supported, to separateWordSearch should be false type pagefindHighlightOptions = { - markContext: string | HTMLElement | HTMLElement[] | NodeList | null; - pagefindQueryParamName: string; - markOptions: Omit; - addStyles: boolean; + markContext?: string | HTMLElement | HTMLElement[] | NodeList | null; + pagefindQueryParamName?: string; + markOptions?: Omit; + addStyles?: boolean; }; export default class PagefindHighlight { @@ -37,12 +37,19 @@ export default class PagefindHighlight { const { pagefindQueryParamName, markContext, markOptions, addStyles } = options; - this.pagefindQueryParamName = pagefindQueryParamName; - this.addStyles = addStyles; - this.markContext = markContext; - this.markOptions = markOptions; + this.pagefindQueryParamName = + pagefindQueryParamName ?? "pagefind-highlight"; + this.addStyles = addStyles ?? true; + this.markContext = markContext !== undefined ? markContext : null; + this.markOptions = + markOptions !== undefined + ? markOptions + : { + className: "pagefind-highlight", + exclude: ["[data-pagefind-ignore]", "[data-pagefind-ignore] *"], + }; - // make sure these are always set + // make sure these are always set (in case the user passes {} or {exclude: '.exclude'} to markOptions) // if the user doesn't want to exclude anything, they should pass an empty array // if the user doesn't want a className they should pass an empty string this.markOptions.className ??= "pagefind__highlight"; @@ -94,4 +101,10 @@ export default class PagefindHighlight { } } +declare global { + interface Window { + PagefindHighlight: typeof PagefindHighlight; + } +} + window.PagefindHighlight = PagefindHighlight; From 378d132984ead8dea1a7675fdd91a3537e51c397 Mon Sep 17 00:00:00 2001 From: Jothsa <58094796+Jothsa@users.noreply.github.com> Date: Sun, 17 Sep 2023 13:02:25 -0500 Subject: [PATCH 18/18] some docs --- docs/content/docs/_index.md | 13 ++++++++ docs/content/docs/highlight-config.md | 44 +++++++++++++++++++++++++++ docs/content/docs/highlighting.md | 17 +++++++++++ docs/content/docs/ui.md | 13 ++++++++ 4 files changed, 87 insertions(+) create mode 100644 docs/content/docs/highlight-config.md create mode 100644 docs/content/docs/highlighting.md diff --git a/docs/content/docs/_index.md b/docs/content/docs/_index.md index 274c81fc..027c0c46 100644 --- a/docs/content/docs/_index.md +++ b/docs/content/docs/_index.md @@ -53,6 +53,19 @@ We can see that a bunch of content was indexed, and Pagefind will be running a p Loading this in your browser, you should see a search input on your page. Try searching for some content and you will see results appear from your site. +## Highlighting + +To highlight the search terms on results page, add the following snippet on every page that has been indexed + +```html + + +