From c9fd16899322ba393abfbeb1846be23cf816e53f Mon Sep 17 00:00:00 2001 From: Carl Gieringer <78054+carlgieringer@users.noreply.github.com> Date: Mon, 5 Aug 2024 00:13:54 -0700 Subject: [PATCH] Normalize UrlLocator.domAnchors so that they merge correctly. Signed-off-by: Carl Gieringer <78054+carlgieringer@users.noreply.github.com> --- premiser-ui/src/normalizationSchemas.ts | 23 +++++++++++++++++++++++ premiser-ui/src/reducers/entities.ts | 8 +++++--- premiser-ui/src/util.tsx | 21 +++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/premiser-ui/src/normalizationSchemas.ts b/premiser-ui/src/normalizationSchemas.ts index 23d1056f..2b3857f0 100644 --- a/premiser-ui/src/normalizationSchemas.ts +++ b/premiser-ui/src/normalizationSchemas.ts @@ -6,6 +6,7 @@ import { AppearanceView, ContextTrailItem, Domain, + DomAnchor, JustificationView, JustificationVote, MediaExcerptCitationIdentifier, @@ -38,6 +39,7 @@ import { } from "howdju-common"; import { applyCustomizations, momentConversion } from "./normalizationUtil"; +import { hashString } from "./util"; export const userSchema = new schema.Entity("users"); export const usersSchema = new schema.Array(userSchema); @@ -176,10 +178,31 @@ export const sourceExcerptParaphraseSchema = export const urlSchema = new schema.Entity("urls"); export const urlsSchema = new schema.Array(urlSchema); +export const domAnchorSchema = new schema.Entity( + "domAnchors", + {}, + { + idAttribute: domAnchorKey, + } +); + +function domAnchorKey(anchor: DomAnchor) { + return [ + anchor.urlLocatorId, + anchor.startOffset, + anchor.endOffset, + hashString(anchor.prefixText), + hashString(anchor.exactText), + hashString(anchor.suffixText), + ].join("-"); +} +export const domAnchorsSchema = new schema.Array(domAnchorSchema); + export const urlLocatorSchema = new schema.Entity( "urlLocators", { url: urlSchema, + anchors: domAnchorsSchema, }, { processStrategy: (value) => diff --git a/premiser-ui/src/reducers/entities.ts b/premiser-ui/src/reducers/entities.ts index 340c1a4d..76157376 100644 --- a/premiser-ui/src/reducers/entities.ts +++ b/premiser-ui/src/reducers/entities.ts @@ -37,6 +37,7 @@ import { mediaExcerptSpeakerKey, propositionTagVoteSchema, domainSchema, + domAnchorSchema, } from "@/normalizationSchemas"; import { MergeDeep } from "type-fest"; @@ -91,6 +92,7 @@ export const initialState = { appearances: {} as SchemaEntityState, contextTrailItems: {} as SchemaEntityState, domains: {} as SchemaEntityState, + domAnchors: {} as SchemaEntityState, justifications: {} as SchemaEntityState< typeof justificationSchema, { @@ -174,7 +176,7 @@ const slice = createSlice({ return; } const payload = action.payload as unknown as ApiErrorPayload; - // If a proposition is not found (e.g., another user deleted it), then remove it. + // If a statement is not found (e.g., another user deleted it), then remove it. if (payload.httpStatusCode === httpStatusCodes.NOT_FOUND) { const { rootTargetId } = action.meta.requestMeta; delete state.statements[rootTargetId]; @@ -548,8 +550,8 @@ export function deepMerge(x: Partial, y: Partial): T1 & T2 { } export const deepMergeOptions: DeepMergeOptions = { - // Overwrite arrays. This prevents us from merging arrays of objects, but since we store our - // entities normalized, most objects we care about merging will be top-level entities. + // Union arrays so that we deduplicate related entity IDs. (The default behavior is to + // concatenate arrays elements.) arrayMerge: (targetArray, sourceArray, _options) => union(targetArray, sourceArray), // Don't copy the properties of Moment objects (or else we lose their methods.) diff --git a/premiser-ui/src/util.tsx b/premiser-ui/src/util.tsx index 8dea49c1..78d8c555 100644 --- a/premiser-ui/src/util.tsx +++ b/premiser-ui/src/util.tsx @@ -156,3 +156,24 @@ export function toCompatibleTagVotes( } as TagVote) ); } + +/** + * Hashes a string into a number. + * + * https://github.com/bryc/code/blob/da36a3e07acfbd07f930a9212a2df9e854ff56e4/jshash/experimental/cyrb53.js + */ +export function hashString(str: string, seed = 0) { + let h1 = 0xdeadbeef ^ seed, + h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + + return 4294967296 * (2097151 & h2) + (h1 >>> 0); +}