Skip to content
Open
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
13 changes: 13 additions & 0 deletions apps/web/src/components/files/pdfLayout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ describe("pdfFindMatchRuns", () => {
expect(pdfFindIncludes(page, " ")).toBe(false);
});

it("matches straight quotes typed for typeset curly ones", () => {
expect(pdfFindMatchRuns(["We don\u2019t ", "ship \u201cbeta\u201d"], "don't")).toEqual([
true,
false,
]);
expect(pdfFindMatchRuns(["We don\u2019t ", "ship \u201cbeta\u201d"], '"beta"')).toEqual([
false,
true,
]);
expect(pdfFindIncludes("We don\u2019t ship \u201cbeta\u201d", 'don\'t ship "beta"')).toBe(true);
expect(pdfFindIncludes("We don't ship", "don\u2019t")).toBe(true);
});

it("highlights nothing for a blank or missing query", () => {
expect(pdfFindMatchRuns(["a b", "c"], " ")).toEqual([false, false]);
expect(pdfFindMatchRuns(["a b", "c"], "")).toEqual([false, false]);
Expand Down
14 changes: 9 additions & 5 deletions apps/web/src/components/files/pdfLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,19 @@ export function pdfPageText(
return items.map((item) => ("str" in item ? item.str + (item.hasEOL ? "\n" : "") : "")).join("");
}

// A typed straight quote finds a typeset curly one, as pdf.js's own find does.
function foldQuotes(text: string) {
return text.replace(/[\u2018-\u201b]/g, "'").replace(/[\u201c-\u201f]/g, '"');
Comment thread
saphid marked this conversation as resolved.
Resolved
}

/**
* Lowercased search text with each whitespace run, line ends included, read as one space, and a
* hyphen that ends a line between letters dropped, as pdf.js's own find does. `owner` maps each
* character back to the run it came from.
*/
function pdfFindText(runs: readonly string[]) {
const chars = runs.flatMap((run, index) =>
Array.from(run.toLowerCase(), (char) => ({ char, run: index })),
Array.from(foldQuotes(run.toLowerCase()), (char) => ({ char, run: index })),
);
let text = "";
let last = "";
Expand Down Expand Up @@ -95,10 +100,9 @@ function pdfFindText(runs: readonly string[]) {
/** Whether a page's text holds the query, under the same rules as `pdfFindMatchRuns`, without
* mapping characters back to runs: the page scan checks every page on each query. */
export function pdfFindIncludes(text: string, query: string) {
const needle = query.toLowerCase().replace(/\s+/g, " ");
const needle = foldQuotes(query.toLowerCase()).replace(/\s+/g, " ");
if (!needle.trim()) return false;
return text
.toLowerCase()
return foldQuotes(text.toLowerCase())
.replace(/(\p{L})-\n(?=\p{L})/gu, "$1")
.replace(/\s+/g, " ")
.includes(needle);
Expand All @@ -107,7 +111,7 @@ export function pdfFindIncludes(text: string, query: string) {
/** Which text runs hold part of a case-insensitive match, when pdf.js splits words across spans. */
export function pdfFindMatchRuns(runs: readonly string[], query: string) {
const { text, owner } = pdfFindText(runs);
const needle = query.toLowerCase().replace(/\s+/g, " ");
const needle = foldQuotes(query.toLowerCase()).replace(/\s+/g, " ");
const hits = runs.map(() => false);
if (!needle.trim()) return hits;
for (
Expand Down
Loading