web-remarq 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,6 +57,7 @@ module.exports = __toCommonJS(core_exports);
57
57
 
58
58
  // src/core/hash-detect.ts
59
59
  var CSS_MODULES_RE = /^(.+)__([a-zA-Z0-9]{3,})$/;
60
+ var CSS_MODULES_3SEG_RE = /^([^_]+(?:[_-][^_]+)*)__([a-zA-Z][a-zA-Z0-9]*)__([a-zA-Z0-9]{3,})$/;
60
61
  var STYLED_COMPONENTS_RE = /^sc-/;
61
62
  var EMOTION_RE = /^css-[a-zA-Z0-9]+$/;
62
63
  var PURE_HASH_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9]{8,}$/;
@@ -87,6 +88,20 @@ function filterClasses(classes, classFilter) {
87
88
  }
88
89
  return result;
89
90
  }
91
+ function decomposeCSSModules(classes) {
92
+ const result = [];
93
+ for (const cls of classes) {
94
+ const match = cls.match(CSS_MODULES_3SEG_RE);
95
+ if (match) {
96
+ result.push({
97
+ raw: cls,
98
+ moduleHint: match[1],
99
+ localName: match[2]
100
+ });
101
+ }
102
+ }
103
+ return result;
104
+ }
90
105
 
91
106
  // src/core/fingerprint.ts
92
107
  var TEXT_MAX_LENGTH = 50;
@@ -107,7 +122,9 @@ function createFingerprint(el, options) {
107
122
  ),
108
123
  domPath: buildDomPath(el),
109
124
  siblingIndex: getSiblingIndex(el),
110
- parentAnchor: findParentAnchor(el, dataAttr)
125
+ parentAnchor: findParentAnchor(el, dataAttr),
126
+ rawClasses: Array.from(el.classList),
127
+ cssModules: decomposeCSSModules(Array.from(el.classList))
111
128
  };
112
129
  }
113
130
  function getStableId(el) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/index.ts","../../src/core/hash-detect.ts","../../src/core/fingerprint.ts","../../src/core/matcher.ts","../../src/core/viewport.ts","../../src/core/storage.ts"],"sourcesContent":["export type { Annotation, AnnotationStore, ElementFingerprint } from './types'\nexport { createFingerprint } from './fingerprint'\nexport { matchElement } from './matcher'\nexport { AnnotationStorage } from './storage'\n","const CSS_MODULES_RE = /^(.+)__([a-zA-Z0-9]{3,})$/\nconst STYLED_COMPONENTS_RE = /^sc-/\nconst EMOTION_RE = /^css-[a-zA-Z0-9]+$/\nconst PURE_HASH_RE = /^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z0-9]{8,}$/\n\nexport function isHashedClass(className: string): boolean {\n if (STYLED_COMPONENTS_RE.test(className)) return true\n if (EMOTION_RE.test(className)) return true\n if (CSS_MODULES_RE.test(className)) return true\n if (PURE_HASH_RE.test(className)) return true\n return false\n}\n\nexport function stripHash(className: string): string {\n const match = className.match(CSS_MODULES_RE)\n if (match) {\n const prefix = className.slice(0, className.lastIndexOf('__'))\n return prefix\n }\n return className\n}\n\nexport function filterClasses(\n classes: string[],\n classFilter?: (className: string) => boolean,\n): string[] {\n const result: string[] = []\n\n for (const cls of classes) {\n if (STYLED_COMPONENTS_RE.test(cls)) continue\n if (EMOTION_RE.test(cls)) continue\n if (PURE_HASH_RE.test(cls)) continue\n\n let stable = stripHash(cls)\n\n if (classFilter && !classFilter(stable)) continue\n\n result.push(stable)\n }\n\n return result\n}\n","import type { ElementFingerprint, WebRemarqOptions } from './types'\nimport { filterClasses, isHashedClass } from './hash-detect'\n\nconst TEXT_MAX_LENGTH = 50\n\nexport function createFingerprint(\n el: HTMLElement,\n options?: Pick<WebRemarqOptions, 'classFilter' | 'dataAttribute'>,\n): ElementFingerprint {\n const dataAttr = options?.dataAttribute ?? 'data-annotate'\n\n return {\n dataAnnotate: el.getAttribute(dataAttr) ?? null,\n dataTestId: el.getAttribute('data-testid')\n ?? el.getAttribute('data-test')\n ?? el.getAttribute('data-cy')\n ?? null,\n id: getStableId(el),\n tagName: el.tagName.toLowerCase(),\n textContent: getTextContent(el),\n role: el.getAttribute('role') ?? null,\n ariaLabel: el.getAttribute('aria-label') ?? null,\n stableClasses: filterClasses(\n Array.from(el.classList),\n options?.classFilter,\n ),\n domPath: buildDomPath(el),\n siblingIndex: getSiblingIndex(el),\n parentAnchor: findParentAnchor(el, dataAttr),\n }\n}\n\nfunction getStableId(el: HTMLElement): string | null {\n const id = el.id\n if (!id) return null\n if (isHashedClass(id)) return null\n return id\n}\n\nfunction getTextContent(el: HTMLElement): string | null {\n // Use only direct text nodes, not nested children's text\n let text = ''\n for (const node of Array.from(el.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n text += node.textContent ?? ''\n }\n }\n text = text.trim()\n\n // If no direct text, try first meaningful child's text (for wrappers like <span><b>Text</b></span>)\n if (!text && el.children.length <= 3) {\n text = el.textContent?.trim() ?? ''\n }\n\n if (!text) return null\n return text.length > TEXT_MAX_LENGTH ? text.slice(0, TEXT_MAX_LENGTH) : text\n}\n\nfunction buildDomPath(el: HTMLElement): string {\n const parts: string[] = []\n let current: HTMLElement | null = el\n\n while (current && current !== document.body && parts.length < 5) {\n let segment = current.tagName.toLowerCase()\n const stable = filterClasses(Array.from(current.classList))\n if (stable.length > 0) {\n segment += '.' + stable.slice(0, 2).join('.')\n }\n parts.unshift(segment)\n current = current.parentElement\n }\n\n return parts.join(' > ')\n}\n\nfunction getSiblingIndex(el: HTMLElement): number {\n const parent = el.parentElement\n if (!parent) return 0\n const children = Array.from(parent.children)\n return children.indexOf(el)\n}\n\nfunction findParentAnchor(el: HTMLElement, dataAttr: string): string | null {\n let current = el.parentElement\n while (current && current !== document.body) {\n const value = current.getAttribute(dataAttr)\n if (value) return value\n current = current.parentElement\n }\n return null\n}\n","import type { ElementFingerprint, WebRemarqOptions } from './types'\nimport { filterClasses } from './hash-detect'\n\nconst MATCH_THRESHOLD = 50\n\nexport function levenshteinSimilarity(a: string, b: string): number {\n if (a === b) return 1\n if (!a.length || !b.length) return 0\n\n const matrix: number[][] = []\n for (let i = 0; i <= a.length; i++) {\n matrix[i] = [i]\n }\n for (let j = 0; j <= b.length; j++) {\n matrix[0][j] = j\n }\n\n for (let i = 1; i <= a.length; i++) {\n for (let j = 1; j <= b.length; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n matrix[i][j] = Math.min(\n matrix[i - 1][j] + 1,\n matrix[i][j - 1] + 1,\n matrix[i - 1][j - 1] + cost,\n )\n }\n }\n\n const distance = matrix[a.length][b.length]\n return 1 - distance / Math.max(a.length, b.length)\n}\n\nfunction textSimilarity(a: string | null, b: string | null): number {\n if (!a || !b) return 0\n const na = a.trim().toLowerCase()\n const nb = b.trim().toLowerCase()\n if (na === nb) return 1\n if (na.includes(nb) || nb.includes(na)) return 1\n return levenshteinSimilarity(na, nb)\n}\n\nfunction jaccardSimilarity(a: string[], b: string[]): number {\n if (!a.length && !b.length) return 0\n const setA = new Set(a)\n const setB = new Set(b)\n let intersection = 0\n for (const item of setA) {\n if (setB.has(item)) intersection++\n }\n const union = new Set([...a, ...b]).size\n return union === 0 ? 0 : intersection / union\n}\n\nfunction scoreCandidate(el: HTMLElement, fp: ElementFingerprint, dataAttr: string): number {\n let score = 0\n\n // dataAnnotate match (+100)\n const elAnnotate = el.getAttribute(dataAttr)\n if (fp.dataAnnotate && elAnnotate === fp.dataAnnotate) {\n score += 100\n }\n\n // textContent match (+35 scaled)\n const elText = el.textContent?.trim().slice(0, 50) ?? null\n const textSim = textSimilarity(fp.textContent, elText)\n if (textSim > 0.7) {\n score += textSim * 35\n }\n\n // role + ariaLabel match (+30)\n if (fp.role && el.getAttribute('role') === fp.role &&\n fp.ariaLabel && el.getAttribute('aria-label') === fp.ariaLabel) {\n score += 30\n }\n\n // parentAnchor match (+15)\n if (fp.parentAnchor) {\n let parent = el.parentElement\n while (parent && parent !== document.body) {\n if (parent.getAttribute(dataAttr) === fp.parentAnchor) {\n score += 15\n break\n }\n parent = parent.parentElement\n }\n }\n\n // stableClasses overlap (+30 scaled)\n if (fp.stableClasses.length > 0) {\n const elClasses = filterClasses(Array.from(el.classList))\n const jaccard = jaccardSimilarity(fp.stableClasses, elClasses)\n score += jaccard * 30\n }\n\n // domPath match (+20 scaled)\n if (fp.domPath) {\n const elPath = buildDomPath(el)\n const pathSim = levenshteinSimilarity(fp.domPath, elPath)\n score += pathSim * 20\n }\n\n // siblingIndex match (+5)\n const parent = el.parentElement\n if (parent) {\n const idx = Array.from(parent.children).indexOf(el)\n if (idx === fp.siblingIndex) {\n score += 5\n }\n }\n\n return score\n}\n\nfunction buildDomPath(el: HTMLElement): string {\n const parts: string[] = []\n let current: HTMLElement | null = el\n while (current && current !== document.body && parts.length < 5) {\n let segment = current.tagName.toLowerCase()\n const stable = filterClasses(Array.from(current.classList))\n if (stable.length > 0) {\n segment += '.' + stable.slice(0, 2).join('.')\n }\n parts.unshift(segment)\n current = current.parentElement\n }\n return parts.join(' > ')\n}\n\nexport function matchElement(\n fp: ElementFingerprint,\n options?: Pick<WebRemarqOptions, 'dataAttribute'>,\n): HTMLElement | null {\n const dataAttr = options?.dataAttribute ?? 'data-annotate'\n\n // 1. Exact match by data-annotate\n if (fp.dataAnnotate) {\n const el = document.querySelector<HTMLElement>(`[${dataAttr}=\"${fp.dataAnnotate}\"]`)\n if (el) return el\n }\n\n // 2. Exact match by data-testid\n if (fp.dataTestId) {\n const el = document.querySelector<HTMLElement>(\n `[data-testid=\"${fp.dataTestId}\"], [data-test=\"${fp.dataTestId}\"], [data-cy=\"${fp.dataTestId}\"]`,\n )\n if (el) return el\n }\n\n // 3. Exact match by id\n if (fp.id) {\n const el = document.getElementById(fp.id) as HTMLElement | null\n if (el) return el\n }\n\n // 4. Fuzzy match by tagName + weighted scoring\n const candidates = document.querySelectorAll<HTMLElement>(fp.tagName)\n let bestEl: HTMLElement | null = null\n let bestScore = 0\n\n for (const candidate of candidates) {\n const score = scoreCandidate(candidate, fp, dataAttr)\n if (score > bestScore) {\n bestScore = score\n bestEl = candidate\n }\n }\n\n return bestScore >= MATCH_THRESHOLD ? bestEl : null\n}\n","type BucketChangeCallback = () => void\n\nlet currentBucket: number = 0\nlet onBucketChange: BucketChangeCallback | null = null\nlet resizeHandler: (() => void) | null = null\n\nexport function toBucket(width: number): number {\n return Math.floor(width / 100) * 100\n}\n\nfunction debounce(fn: () => void, ms: number): () => void {\n let timer: ReturnType<typeof setTimeout>\n return () => {\n clearTimeout(timer)\n timer = setTimeout(fn, ms)\n }\n}\n\nexport function initViewportListener(callback: BucketChangeCallback): void {\n currentBucket = toBucket(window.innerWidth)\n onBucketChange = callback\n\n resizeHandler = debounce(() => {\n const newBucket = toBucket(window.innerWidth)\n if (newBucket !== currentBucket) {\n currentBucket = newBucket\n onBucketChange?.()\n }\n }, 300)\n\n window.addEventListener('resize', resizeHandler)\n}\n\nexport function destroyViewportListener(): void {\n if (resizeHandler) {\n window.removeEventListener('resize', resizeHandler)\n resizeHandler = null\n }\n onBucketChange = null\n}\n","import type { Annotation, AnnotationStore } from './types'\nimport { toBucket } from './viewport'\n\nconst STORAGE_KEY = 'remarq:annotations'\n\nexport class AnnotationStorage {\n private annotations: Annotation[] = []\n private extraFields: Record<string, unknown> = {}\n isMemoryOnly = false\n\n constructor() {\n this.load()\n }\n\n getAll(): Annotation[] {\n return [...this.annotations]\n }\n\n getByRoute(route: string): Annotation[] {\n return this.annotations.filter((a) => a.route === route)\n }\n\n add(annotation: Annotation): void {\n this.annotations.push(annotation)\n this.save()\n }\n\n remove(id: string): void {\n this.annotations = this.annotations.filter((a) => a.id !== id)\n this.save()\n }\n\n update(id: string, changes: Partial<Annotation>): void {\n const idx = this.annotations.findIndex((a) => a.id === id)\n if (idx !== -1) {\n this.annotations[idx] = { ...this.annotations[idx], ...changes }\n this.save()\n }\n }\n\n clearAll(): void {\n this.annotations = []\n this.save()\n }\n\n exportJSON(): AnnotationStore {\n return {\n version: 1,\n annotations: [...this.annotations],\n }\n }\n\n importJSON(data: AnnotationStore): void {\n this.annotations = [...data.annotations]\n this.migrateViewportBuckets()\n this.save()\n }\n\n private migrateViewportBuckets(): void {\n for (const ann of this.annotations) {\n if (ann.viewportBucket == null && ann.viewport) {\n const width = parseInt(ann.viewport.split('x')[0], 10)\n ann.viewportBucket = toBucket(width)\n }\n }\n }\n\n private load(): void {\n try {\n const raw = localStorage.getItem(STORAGE_KEY)\n if (raw) {\n const parsed = JSON.parse(raw)\n const { version, annotations, ...rest } = parsed\n this.annotations = annotations ?? []\n this.extraFields = rest\n this.migrateViewportBuckets()\n }\n } catch {\n this.isMemoryOnly = true\n }\n }\n\n private save(): void {\n if (this.isMemoryOnly) return\n try {\n const data = {\n version: 1,\n ...this.extraFields,\n annotations: this.annotations,\n }\n localStorage.setItem(STORAGE_KEY, JSON.stringify(data))\n } catch {\n this.isMemoryOnly = true\n }\n }\n}\n"],"mappings":"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","names":["parent","buildDomPath"]}
1
+ {"version":3,"sources":["../../src/core/index.ts","../../src/core/hash-detect.ts","../../src/core/fingerprint.ts","../../src/core/matcher.ts","../../src/core/viewport.ts","../../src/core/storage.ts"],"sourcesContent":["export type { Annotation, AnnotationStore, ElementFingerprint, CSSModuleClass } from './types'\nexport { createFingerprint } from './fingerprint'\nexport { matchElement } from './matcher'\nexport { AnnotationStorage } from './storage'\n","import type { CSSModuleClass } from './types'\n\nconst CSS_MODULES_RE = /^(.+)__([a-zA-Z0-9]{3,})$/\nconst CSS_MODULES_3SEG_RE = /^([^_]+(?:[_-][^_]+)*)__([a-zA-Z][a-zA-Z0-9]*)__([a-zA-Z0-9]{3,})$/\nconst STYLED_COMPONENTS_RE = /^sc-/\nconst EMOTION_RE = /^css-[a-zA-Z0-9]+$/\nconst PURE_HASH_RE = /^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z0-9]{8,}$/\n\nexport function isHashedClass(className: string): boolean {\n if (STYLED_COMPONENTS_RE.test(className)) return true\n if (EMOTION_RE.test(className)) return true\n if (CSS_MODULES_RE.test(className)) return true\n if (PURE_HASH_RE.test(className)) return true\n return false\n}\n\nexport function stripHash(className: string): string {\n const match = className.match(CSS_MODULES_RE)\n if (match) {\n const prefix = className.slice(0, className.lastIndexOf('__'))\n return prefix\n }\n return className\n}\n\nexport function filterClasses(\n classes: string[],\n classFilter?: (className: string) => boolean,\n): string[] {\n const result: string[] = []\n\n for (const cls of classes) {\n if (STYLED_COMPONENTS_RE.test(cls)) continue\n if (EMOTION_RE.test(cls)) continue\n if (PURE_HASH_RE.test(cls)) continue\n\n let stable = stripHash(cls)\n\n if (classFilter && !classFilter(stable)) continue\n\n result.push(stable)\n }\n\n return result\n}\n\nexport function decomposeCSSModules(classes: string[]): CSSModuleClass[] {\n const result: CSSModuleClass[] = []\n for (const cls of classes) {\n const match = cls.match(CSS_MODULES_3SEG_RE)\n if (match) {\n result.push({\n raw: cls,\n moduleHint: match[1],\n localName: match[2],\n })\n }\n }\n return result\n}\n","import type { ElementFingerprint, WebRemarqOptions } from './types'\nimport { filterClasses, isHashedClass, decomposeCSSModules } from './hash-detect'\n\nconst TEXT_MAX_LENGTH = 50\n\nexport function createFingerprint(\n el: HTMLElement,\n options?: Pick<WebRemarqOptions, 'classFilter' | 'dataAttribute'>,\n): ElementFingerprint {\n const dataAttr = options?.dataAttribute ?? 'data-annotate'\n\n return {\n dataAnnotate: el.getAttribute(dataAttr) ?? null,\n dataTestId: el.getAttribute('data-testid')\n ?? el.getAttribute('data-test')\n ?? el.getAttribute('data-cy')\n ?? null,\n id: getStableId(el),\n tagName: el.tagName.toLowerCase(),\n textContent: getTextContent(el),\n role: el.getAttribute('role') ?? null,\n ariaLabel: el.getAttribute('aria-label') ?? null,\n stableClasses: filterClasses(\n Array.from(el.classList),\n options?.classFilter,\n ),\n domPath: buildDomPath(el),\n siblingIndex: getSiblingIndex(el),\n parentAnchor: findParentAnchor(el, dataAttr),\n rawClasses: Array.from(el.classList),\n cssModules: decomposeCSSModules(Array.from(el.classList)),\n }\n}\n\nfunction getStableId(el: HTMLElement): string | null {\n const id = el.id\n if (!id) return null\n if (isHashedClass(id)) return null\n return id\n}\n\nfunction getTextContent(el: HTMLElement): string | null {\n // Use only direct text nodes, not nested children's text\n let text = ''\n for (const node of Array.from(el.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n text += node.textContent ?? ''\n }\n }\n text = text.trim()\n\n // If no direct text, try first meaningful child's text (for wrappers like <span><b>Text</b></span>)\n if (!text && el.children.length <= 3) {\n text = el.textContent?.trim() ?? ''\n }\n\n if (!text) return null\n return text.length > TEXT_MAX_LENGTH ? text.slice(0, TEXT_MAX_LENGTH) : text\n}\n\nfunction buildDomPath(el: HTMLElement): string {\n const parts: string[] = []\n let current: HTMLElement | null = el\n\n while (current && current !== document.body && parts.length < 5) {\n let segment = current.tagName.toLowerCase()\n const stable = filterClasses(Array.from(current.classList))\n if (stable.length > 0) {\n segment += '.' + stable.slice(0, 2).join('.')\n }\n parts.unshift(segment)\n current = current.parentElement\n }\n\n return parts.join(' > ')\n}\n\nfunction getSiblingIndex(el: HTMLElement): number {\n const parent = el.parentElement\n if (!parent) return 0\n const children = Array.from(parent.children)\n return children.indexOf(el)\n}\n\nfunction findParentAnchor(el: HTMLElement, dataAttr: string): string | null {\n let current = el.parentElement\n while (current && current !== document.body) {\n const value = current.getAttribute(dataAttr)\n if (value) return value\n current = current.parentElement\n }\n return null\n}\n","import type { ElementFingerprint, WebRemarqOptions } from './types'\nimport { filterClasses } from './hash-detect'\n\nconst MATCH_THRESHOLD = 50\n\nexport function levenshteinSimilarity(a: string, b: string): number {\n if (a === b) return 1\n if (!a.length || !b.length) return 0\n\n const matrix: number[][] = []\n for (let i = 0; i <= a.length; i++) {\n matrix[i] = [i]\n }\n for (let j = 0; j <= b.length; j++) {\n matrix[0][j] = j\n }\n\n for (let i = 1; i <= a.length; i++) {\n for (let j = 1; j <= b.length; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n matrix[i][j] = Math.min(\n matrix[i - 1][j] + 1,\n matrix[i][j - 1] + 1,\n matrix[i - 1][j - 1] + cost,\n )\n }\n }\n\n const distance = matrix[a.length][b.length]\n return 1 - distance / Math.max(a.length, b.length)\n}\n\nfunction textSimilarity(a: string | null, b: string | null): number {\n if (!a || !b) return 0\n const na = a.trim().toLowerCase()\n const nb = b.trim().toLowerCase()\n if (na === nb) return 1\n if (na.includes(nb) || nb.includes(na)) return 1\n return levenshteinSimilarity(na, nb)\n}\n\nfunction jaccardSimilarity(a: string[], b: string[]): number {\n if (!a.length && !b.length) return 0\n const setA = new Set(a)\n const setB = new Set(b)\n let intersection = 0\n for (const item of setA) {\n if (setB.has(item)) intersection++\n }\n const union = new Set([...a, ...b]).size\n return union === 0 ? 0 : intersection / union\n}\n\nfunction scoreCandidate(el: HTMLElement, fp: ElementFingerprint, dataAttr: string): number {\n let score = 0\n\n // dataAnnotate match (+100)\n const elAnnotate = el.getAttribute(dataAttr)\n if (fp.dataAnnotate && elAnnotate === fp.dataAnnotate) {\n score += 100\n }\n\n // textContent match (+35 scaled)\n const elText = el.textContent?.trim().slice(0, 50) ?? null\n const textSim = textSimilarity(fp.textContent, elText)\n if (textSim > 0.7) {\n score += textSim * 35\n }\n\n // role + ariaLabel match (+30)\n if (fp.role && el.getAttribute('role') === fp.role &&\n fp.ariaLabel && el.getAttribute('aria-label') === fp.ariaLabel) {\n score += 30\n }\n\n // parentAnchor match (+15)\n if (fp.parentAnchor) {\n let parent = el.parentElement\n while (parent && parent !== document.body) {\n if (parent.getAttribute(dataAttr) === fp.parentAnchor) {\n score += 15\n break\n }\n parent = parent.parentElement\n }\n }\n\n // stableClasses overlap (+30 scaled)\n if (fp.stableClasses.length > 0) {\n const elClasses = filterClasses(Array.from(el.classList))\n const jaccard = jaccardSimilarity(fp.stableClasses, elClasses)\n score += jaccard * 30\n }\n\n // domPath match (+20 scaled)\n if (fp.domPath) {\n const elPath = buildDomPath(el)\n const pathSim = levenshteinSimilarity(fp.domPath, elPath)\n score += pathSim * 20\n }\n\n // siblingIndex match (+5)\n const parent = el.parentElement\n if (parent) {\n const idx = Array.from(parent.children).indexOf(el)\n if (idx === fp.siblingIndex) {\n score += 5\n }\n }\n\n return score\n}\n\nfunction buildDomPath(el: HTMLElement): string {\n const parts: string[] = []\n let current: HTMLElement | null = el\n while (current && current !== document.body && parts.length < 5) {\n let segment = current.tagName.toLowerCase()\n const stable = filterClasses(Array.from(current.classList))\n if (stable.length > 0) {\n segment += '.' + stable.slice(0, 2).join('.')\n }\n parts.unshift(segment)\n current = current.parentElement\n }\n return parts.join(' > ')\n}\n\nexport function matchElement(\n fp: ElementFingerprint,\n options?: Pick<WebRemarqOptions, 'dataAttribute'>,\n): HTMLElement | null {\n const dataAttr = options?.dataAttribute ?? 'data-annotate'\n\n // 1. Exact match by data-annotate\n if (fp.dataAnnotate) {\n const el = document.querySelector<HTMLElement>(`[${dataAttr}=\"${fp.dataAnnotate}\"]`)\n if (el) return el\n }\n\n // 2. Exact match by data-testid\n if (fp.dataTestId) {\n const el = document.querySelector<HTMLElement>(\n `[data-testid=\"${fp.dataTestId}\"], [data-test=\"${fp.dataTestId}\"], [data-cy=\"${fp.dataTestId}\"]`,\n )\n if (el) return el\n }\n\n // 3. Exact match by id\n if (fp.id) {\n const el = document.getElementById(fp.id) as HTMLElement | null\n if (el) return el\n }\n\n // 4. Fuzzy match by tagName + weighted scoring\n const candidates = document.querySelectorAll<HTMLElement>(fp.tagName)\n let bestEl: HTMLElement | null = null\n let bestScore = 0\n\n for (const candidate of candidates) {\n const score = scoreCandidate(candidate, fp, dataAttr)\n if (score > bestScore) {\n bestScore = score\n bestEl = candidate\n }\n }\n\n return bestScore >= MATCH_THRESHOLD ? bestEl : null\n}\n","type BucketChangeCallback = () => void\n\nlet currentBucket: number = 0\nlet onBucketChange: BucketChangeCallback | null = null\nlet resizeHandler: (() => void) | null = null\n\nexport function toBucket(width: number): number {\n return Math.floor(width / 100) * 100\n}\n\nfunction debounce(fn: () => void, ms: number): () => void {\n let timer: ReturnType<typeof setTimeout>\n return () => {\n clearTimeout(timer)\n timer = setTimeout(fn, ms)\n }\n}\n\nexport function initViewportListener(callback: BucketChangeCallback): void {\n currentBucket = toBucket(window.innerWidth)\n onBucketChange = callback\n\n resizeHandler = debounce(() => {\n const newBucket = toBucket(window.innerWidth)\n if (newBucket !== currentBucket) {\n currentBucket = newBucket\n onBucketChange?.()\n }\n }, 300)\n\n window.addEventListener('resize', resizeHandler)\n}\n\nexport function destroyViewportListener(): void {\n if (resizeHandler) {\n window.removeEventListener('resize', resizeHandler)\n resizeHandler = null\n }\n onBucketChange = null\n}\n","import type { Annotation, AnnotationStore } from './types'\nimport { toBucket } from './viewport'\n\nconst STORAGE_KEY = 'remarq:annotations'\n\nexport class AnnotationStorage {\n private annotations: Annotation[] = []\n private extraFields: Record<string, unknown> = {}\n isMemoryOnly = false\n\n constructor() {\n this.load()\n }\n\n getAll(): Annotation[] {\n return [...this.annotations]\n }\n\n getByRoute(route: string): Annotation[] {\n return this.annotations.filter((a) => a.route === route)\n }\n\n add(annotation: Annotation): void {\n this.annotations.push(annotation)\n this.save()\n }\n\n remove(id: string): void {\n this.annotations = this.annotations.filter((a) => a.id !== id)\n this.save()\n }\n\n update(id: string, changes: Partial<Annotation>): void {\n const idx = this.annotations.findIndex((a) => a.id === id)\n if (idx !== -1) {\n this.annotations[idx] = { ...this.annotations[idx], ...changes }\n this.save()\n }\n }\n\n clearAll(): void {\n this.annotations = []\n this.save()\n }\n\n exportJSON(): AnnotationStore {\n return {\n version: 1,\n annotations: [...this.annotations],\n }\n }\n\n importJSON(data: AnnotationStore): void {\n this.annotations = [...data.annotations]\n this.migrateViewportBuckets()\n this.save()\n }\n\n private migrateViewportBuckets(): void {\n for (const ann of this.annotations) {\n if (ann.viewportBucket == null && ann.viewport) {\n const width = parseInt(ann.viewport.split('x')[0], 10)\n ann.viewportBucket = toBucket(width)\n }\n }\n }\n\n private load(): void {\n try {\n const raw = localStorage.getItem(STORAGE_KEY)\n if (raw) {\n const parsed = JSON.parse(raw)\n const { version, annotations, ...rest } = parsed\n this.annotations = annotations ?? []\n this.extraFields = rest\n this.migrateViewportBuckets()\n }\n } catch {\n this.isMemoryOnly = true\n }\n }\n\n private save(): void {\n if (this.isMemoryOnly) return\n try {\n const data = {\n version: 1,\n ...this.extraFields,\n annotations: this.annotations,\n }\n localStorage.setItem(STORAGE_KEY, JSON.stringify(data))\n } catch {\n this.isMemoryOnly = true\n }\n }\n}\n"],"mappings":"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","names":["parent","buildDomPath"]}
@@ -1,3 +1,8 @@
1
+ interface CSSModuleClass {
2
+ raw: string;
3
+ moduleHint: string;
4
+ localName: string;
5
+ }
1
6
  interface ElementFingerprint {
2
7
  dataAnnotate: string | null;
3
8
  dataTestId: string | null;
@@ -10,6 +15,8 @@ interface ElementFingerprint {
10
15
  domPath: string;
11
16
  siblingIndex: number;
12
17
  parentAnchor: string | null;
18
+ rawClasses?: string[];
19
+ cssModules?: CSSModuleClass[];
13
20
  }
14
21
  interface Annotation {
15
22
  id: string;
@@ -53,4 +60,4 @@ declare class AnnotationStorage {
53
60
  private save;
54
61
  }
55
62
 
56
- export { type Annotation, AnnotationStorage, type AnnotationStore, type ElementFingerprint, createFingerprint, matchElement };
63
+ export { type Annotation, AnnotationStorage, type AnnotationStore, type CSSModuleClass, type ElementFingerprint, createFingerprint, matchElement };
@@ -1,3 +1,8 @@
1
+ interface CSSModuleClass {
2
+ raw: string;
3
+ moduleHint: string;
4
+ localName: string;
5
+ }
1
6
  interface ElementFingerprint {
2
7
  dataAnnotate: string | null;
3
8
  dataTestId: string | null;
@@ -10,6 +15,8 @@ interface ElementFingerprint {
10
15
  domPath: string;
11
16
  siblingIndex: number;
12
17
  parentAnchor: string | null;
18
+ rawClasses?: string[];
19
+ cssModules?: CSSModuleClass[];
13
20
  }
14
21
  interface Annotation {
15
22
  id: string;
@@ -53,4 +60,4 @@ declare class AnnotationStorage {
53
60
  private save;
54
61
  }
55
62
 
56
- export { type Annotation, AnnotationStorage, type AnnotationStore, type ElementFingerprint, createFingerprint, matchElement };
63
+ export { type Annotation, AnnotationStorage, type AnnotationStore, type CSSModuleClass, type ElementFingerprint, createFingerprint, matchElement };
@@ -32,6 +32,7 @@ var __objRest = (source, exclude) => {
32
32
 
33
33
  // src/core/hash-detect.ts
34
34
  var CSS_MODULES_RE = /^(.+)__([a-zA-Z0-9]{3,})$/;
35
+ var CSS_MODULES_3SEG_RE = /^([^_]+(?:[_-][^_]+)*)__([a-zA-Z][a-zA-Z0-9]*)__([a-zA-Z0-9]{3,})$/;
35
36
  var STYLED_COMPONENTS_RE = /^sc-/;
36
37
  var EMOTION_RE = /^css-[a-zA-Z0-9]+$/;
37
38
  var PURE_HASH_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9]{8,}$/;
@@ -62,6 +63,20 @@ function filterClasses(classes, classFilter) {
62
63
  }
63
64
  return result;
64
65
  }
66
+ function decomposeCSSModules(classes) {
67
+ const result = [];
68
+ for (const cls of classes) {
69
+ const match = cls.match(CSS_MODULES_3SEG_RE);
70
+ if (match) {
71
+ result.push({
72
+ raw: cls,
73
+ moduleHint: match[1],
74
+ localName: match[2]
75
+ });
76
+ }
77
+ }
78
+ return result;
79
+ }
65
80
 
66
81
  // src/core/fingerprint.ts
67
82
  var TEXT_MAX_LENGTH = 50;
@@ -82,7 +97,9 @@ function createFingerprint(el, options) {
82
97
  ),
83
98
  domPath: buildDomPath(el),
84
99
  siblingIndex: getSiblingIndex(el),
85
- parentAnchor: findParentAnchor(el, dataAttr)
100
+ parentAnchor: findParentAnchor(el, dataAttr),
101
+ rawClasses: Array.from(el.classList),
102
+ cssModules: decomposeCSSModules(Array.from(el.classList))
86
103
  };
87
104
  }
88
105
  function getStableId(el) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/hash-detect.ts","../../src/core/fingerprint.ts","../../src/core/matcher.ts","../../src/core/viewport.ts","../../src/core/storage.ts"],"sourcesContent":["const CSS_MODULES_RE = /^(.+)__([a-zA-Z0-9]{3,})$/\nconst STYLED_COMPONENTS_RE = /^sc-/\nconst EMOTION_RE = /^css-[a-zA-Z0-9]+$/\nconst PURE_HASH_RE = /^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z0-9]{8,}$/\n\nexport function isHashedClass(className: string): boolean {\n if (STYLED_COMPONENTS_RE.test(className)) return true\n if (EMOTION_RE.test(className)) return true\n if (CSS_MODULES_RE.test(className)) return true\n if (PURE_HASH_RE.test(className)) return true\n return false\n}\n\nexport function stripHash(className: string): string {\n const match = className.match(CSS_MODULES_RE)\n if (match) {\n const prefix = className.slice(0, className.lastIndexOf('__'))\n return prefix\n }\n return className\n}\n\nexport function filterClasses(\n classes: string[],\n classFilter?: (className: string) => boolean,\n): string[] {\n const result: string[] = []\n\n for (const cls of classes) {\n if (STYLED_COMPONENTS_RE.test(cls)) continue\n if (EMOTION_RE.test(cls)) continue\n if (PURE_HASH_RE.test(cls)) continue\n\n let stable = stripHash(cls)\n\n if (classFilter && !classFilter(stable)) continue\n\n result.push(stable)\n }\n\n return result\n}\n","import type { ElementFingerprint, WebRemarqOptions } from './types'\nimport { filterClasses, isHashedClass } from './hash-detect'\n\nconst TEXT_MAX_LENGTH = 50\n\nexport function createFingerprint(\n el: HTMLElement,\n options?: Pick<WebRemarqOptions, 'classFilter' | 'dataAttribute'>,\n): ElementFingerprint {\n const dataAttr = options?.dataAttribute ?? 'data-annotate'\n\n return {\n dataAnnotate: el.getAttribute(dataAttr) ?? null,\n dataTestId: el.getAttribute('data-testid')\n ?? el.getAttribute('data-test')\n ?? el.getAttribute('data-cy')\n ?? null,\n id: getStableId(el),\n tagName: el.tagName.toLowerCase(),\n textContent: getTextContent(el),\n role: el.getAttribute('role') ?? null,\n ariaLabel: el.getAttribute('aria-label') ?? null,\n stableClasses: filterClasses(\n Array.from(el.classList),\n options?.classFilter,\n ),\n domPath: buildDomPath(el),\n siblingIndex: getSiblingIndex(el),\n parentAnchor: findParentAnchor(el, dataAttr),\n }\n}\n\nfunction getStableId(el: HTMLElement): string | null {\n const id = el.id\n if (!id) return null\n if (isHashedClass(id)) return null\n return id\n}\n\nfunction getTextContent(el: HTMLElement): string | null {\n // Use only direct text nodes, not nested children's text\n let text = ''\n for (const node of Array.from(el.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n text += node.textContent ?? ''\n }\n }\n text = text.trim()\n\n // If no direct text, try first meaningful child's text (for wrappers like <span><b>Text</b></span>)\n if (!text && el.children.length <= 3) {\n text = el.textContent?.trim() ?? ''\n }\n\n if (!text) return null\n return text.length > TEXT_MAX_LENGTH ? text.slice(0, TEXT_MAX_LENGTH) : text\n}\n\nfunction buildDomPath(el: HTMLElement): string {\n const parts: string[] = []\n let current: HTMLElement | null = el\n\n while (current && current !== document.body && parts.length < 5) {\n let segment = current.tagName.toLowerCase()\n const stable = filterClasses(Array.from(current.classList))\n if (stable.length > 0) {\n segment += '.' + stable.slice(0, 2).join('.')\n }\n parts.unshift(segment)\n current = current.parentElement\n }\n\n return parts.join(' > ')\n}\n\nfunction getSiblingIndex(el: HTMLElement): number {\n const parent = el.parentElement\n if (!parent) return 0\n const children = Array.from(parent.children)\n return children.indexOf(el)\n}\n\nfunction findParentAnchor(el: HTMLElement, dataAttr: string): string | null {\n let current = el.parentElement\n while (current && current !== document.body) {\n const value = current.getAttribute(dataAttr)\n if (value) return value\n current = current.parentElement\n }\n return null\n}\n","import type { ElementFingerprint, WebRemarqOptions } from './types'\nimport { filterClasses } from './hash-detect'\n\nconst MATCH_THRESHOLD = 50\n\nexport function levenshteinSimilarity(a: string, b: string): number {\n if (a === b) return 1\n if (!a.length || !b.length) return 0\n\n const matrix: number[][] = []\n for (let i = 0; i <= a.length; i++) {\n matrix[i] = [i]\n }\n for (let j = 0; j <= b.length; j++) {\n matrix[0][j] = j\n }\n\n for (let i = 1; i <= a.length; i++) {\n for (let j = 1; j <= b.length; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n matrix[i][j] = Math.min(\n matrix[i - 1][j] + 1,\n matrix[i][j - 1] + 1,\n matrix[i - 1][j - 1] + cost,\n )\n }\n }\n\n const distance = matrix[a.length][b.length]\n return 1 - distance / Math.max(a.length, b.length)\n}\n\nfunction textSimilarity(a: string | null, b: string | null): number {\n if (!a || !b) return 0\n const na = a.trim().toLowerCase()\n const nb = b.trim().toLowerCase()\n if (na === nb) return 1\n if (na.includes(nb) || nb.includes(na)) return 1\n return levenshteinSimilarity(na, nb)\n}\n\nfunction jaccardSimilarity(a: string[], b: string[]): number {\n if (!a.length && !b.length) return 0\n const setA = new Set(a)\n const setB = new Set(b)\n let intersection = 0\n for (const item of setA) {\n if (setB.has(item)) intersection++\n }\n const union = new Set([...a, ...b]).size\n return union === 0 ? 0 : intersection / union\n}\n\nfunction scoreCandidate(el: HTMLElement, fp: ElementFingerprint, dataAttr: string): number {\n let score = 0\n\n // dataAnnotate match (+100)\n const elAnnotate = el.getAttribute(dataAttr)\n if (fp.dataAnnotate && elAnnotate === fp.dataAnnotate) {\n score += 100\n }\n\n // textContent match (+35 scaled)\n const elText = el.textContent?.trim().slice(0, 50) ?? null\n const textSim = textSimilarity(fp.textContent, elText)\n if (textSim > 0.7) {\n score += textSim * 35\n }\n\n // role + ariaLabel match (+30)\n if (fp.role && el.getAttribute('role') === fp.role &&\n fp.ariaLabel && el.getAttribute('aria-label') === fp.ariaLabel) {\n score += 30\n }\n\n // parentAnchor match (+15)\n if (fp.parentAnchor) {\n let parent = el.parentElement\n while (parent && parent !== document.body) {\n if (parent.getAttribute(dataAttr) === fp.parentAnchor) {\n score += 15\n break\n }\n parent = parent.parentElement\n }\n }\n\n // stableClasses overlap (+30 scaled)\n if (fp.stableClasses.length > 0) {\n const elClasses = filterClasses(Array.from(el.classList))\n const jaccard = jaccardSimilarity(fp.stableClasses, elClasses)\n score += jaccard * 30\n }\n\n // domPath match (+20 scaled)\n if (fp.domPath) {\n const elPath = buildDomPath(el)\n const pathSim = levenshteinSimilarity(fp.domPath, elPath)\n score += pathSim * 20\n }\n\n // siblingIndex match (+5)\n const parent = el.parentElement\n if (parent) {\n const idx = Array.from(parent.children).indexOf(el)\n if (idx === fp.siblingIndex) {\n score += 5\n }\n }\n\n return score\n}\n\nfunction buildDomPath(el: HTMLElement): string {\n const parts: string[] = []\n let current: HTMLElement | null = el\n while (current && current !== document.body && parts.length < 5) {\n let segment = current.tagName.toLowerCase()\n const stable = filterClasses(Array.from(current.classList))\n if (stable.length > 0) {\n segment += '.' + stable.slice(0, 2).join('.')\n }\n parts.unshift(segment)\n current = current.parentElement\n }\n return parts.join(' > ')\n}\n\nexport function matchElement(\n fp: ElementFingerprint,\n options?: Pick<WebRemarqOptions, 'dataAttribute'>,\n): HTMLElement | null {\n const dataAttr = options?.dataAttribute ?? 'data-annotate'\n\n // 1. Exact match by data-annotate\n if (fp.dataAnnotate) {\n const el = document.querySelector<HTMLElement>(`[${dataAttr}=\"${fp.dataAnnotate}\"]`)\n if (el) return el\n }\n\n // 2. Exact match by data-testid\n if (fp.dataTestId) {\n const el = document.querySelector<HTMLElement>(\n `[data-testid=\"${fp.dataTestId}\"], [data-test=\"${fp.dataTestId}\"], [data-cy=\"${fp.dataTestId}\"]`,\n )\n if (el) return el\n }\n\n // 3. Exact match by id\n if (fp.id) {\n const el = document.getElementById(fp.id) as HTMLElement | null\n if (el) return el\n }\n\n // 4. Fuzzy match by tagName + weighted scoring\n const candidates = document.querySelectorAll<HTMLElement>(fp.tagName)\n let bestEl: HTMLElement | null = null\n let bestScore = 0\n\n for (const candidate of candidates) {\n const score = scoreCandidate(candidate, fp, dataAttr)\n if (score > bestScore) {\n bestScore = score\n bestEl = candidate\n }\n }\n\n return bestScore >= MATCH_THRESHOLD ? bestEl : null\n}\n","type BucketChangeCallback = () => void\n\nlet currentBucket: number = 0\nlet onBucketChange: BucketChangeCallback | null = null\nlet resizeHandler: (() => void) | null = null\n\nexport function toBucket(width: number): number {\n return Math.floor(width / 100) * 100\n}\n\nfunction debounce(fn: () => void, ms: number): () => void {\n let timer: ReturnType<typeof setTimeout>\n return () => {\n clearTimeout(timer)\n timer = setTimeout(fn, ms)\n }\n}\n\nexport function initViewportListener(callback: BucketChangeCallback): void {\n currentBucket = toBucket(window.innerWidth)\n onBucketChange = callback\n\n resizeHandler = debounce(() => {\n const newBucket = toBucket(window.innerWidth)\n if (newBucket !== currentBucket) {\n currentBucket = newBucket\n onBucketChange?.()\n }\n }, 300)\n\n window.addEventListener('resize', resizeHandler)\n}\n\nexport function destroyViewportListener(): void {\n if (resizeHandler) {\n window.removeEventListener('resize', resizeHandler)\n resizeHandler = null\n }\n onBucketChange = null\n}\n","import type { Annotation, AnnotationStore } from './types'\nimport { toBucket } from './viewport'\n\nconst STORAGE_KEY = 'remarq:annotations'\n\nexport class AnnotationStorage {\n private annotations: Annotation[] = []\n private extraFields: Record<string, unknown> = {}\n isMemoryOnly = false\n\n constructor() {\n this.load()\n }\n\n getAll(): Annotation[] {\n return [...this.annotations]\n }\n\n getByRoute(route: string): Annotation[] {\n return this.annotations.filter((a) => a.route === route)\n }\n\n add(annotation: Annotation): void {\n this.annotations.push(annotation)\n this.save()\n }\n\n remove(id: string): void {\n this.annotations = this.annotations.filter((a) => a.id !== id)\n this.save()\n }\n\n update(id: string, changes: Partial<Annotation>): void {\n const idx = this.annotations.findIndex((a) => a.id === id)\n if (idx !== -1) {\n this.annotations[idx] = { ...this.annotations[idx], ...changes }\n this.save()\n }\n }\n\n clearAll(): void {\n this.annotations = []\n this.save()\n }\n\n exportJSON(): AnnotationStore {\n return {\n version: 1,\n annotations: [...this.annotations],\n }\n }\n\n importJSON(data: AnnotationStore): void {\n this.annotations = [...data.annotations]\n this.migrateViewportBuckets()\n this.save()\n }\n\n private migrateViewportBuckets(): void {\n for (const ann of this.annotations) {\n if (ann.viewportBucket == null && ann.viewport) {\n const width = parseInt(ann.viewport.split('x')[0], 10)\n ann.viewportBucket = toBucket(width)\n }\n }\n }\n\n private load(): void {\n try {\n const raw = localStorage.getItem(STORAGE_KEY)\n if (raw) {\n const parsed = JSON.parse(raw)\n const { version, annotations, ...rest } = parsed\n this.annotations = annotations ?? []\n this.extraFields = rest\n this.migrateViewportBuckets()\n }\n } catch {\n this.isMemoryOnly = true\n }\n }\n\n private save(): void {\n if (this.isMemoryOnly) return\n try {\n const data = {\n version: 1,\n ...this.extraFields,\n annotations: this.annotations,\n }\n localStorage.setItem(STORAGE_KEY, JSON.stringify(data))\n } catch {\n this.isMemoryOnly = true\n }\n }\n}\n"],"mappings":"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","names":["parent","buildDomPath"]}
1
+ {"version":3,"sources":["../../src/core/hash-detect.ts","../../src/core/fingerprint.ts","../../src/core/matcher.ts","../../src/core/viewport.ts","../../src/core/storage.ts"],"sourcesContent":["import type { CSSModuleClass } from './types'\n\nconst CSS_MODULES_RE = /^(.+)__([a-zA-Z0-9]{3,})$/\nconst CSS_MODULES_3SEG_RE = /^([^_]+(?:[_-][^_]+)*)__([a-zA-Z][a-zA-Z0-9]*)__([a-zA-Z0-9]{3,})$/\nconst STYLED_COMPONENTS_RE = /^sc-/\nconst EMOTION_RE = /^css-[a-zA-Z0-9]+$/\nconst PURE_HASH_RE = /^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z0-9]{8,}$/\n\nexport function isHashedClass(className: string): boolean {\n if (STYLED_COMPONENTS_RE.test(className)) return true\n if (EMOTION_RE.test(className)) return true\n if (CSS_MODULES_RE.test(className)) return true\n if (PURE_HASH_RE.test(className)) return true\n return false\n}\n\nexport function stripHash(className: string): string {\n const match = className.match(CSS_MODULES_RE)\n if (match) {\n const prefix = className.slice(0, className.lastIndexOf('__'))\n return prefix\n }\n return className\n}\n\nexport function filterClasses(\n classes: string[],\n classFilter?: (className: string) => boolean,\n): string[] {\n const result: string[] = []\n\n for (const cls of classes) {\n if (STYLED_COMPONENTS_RE.test(cls)) continue\n if (EMOTION_RE.test(cls)) continue\n if (PURE_HASH_RE.test(cls)) continue\n\n let stable = stripHash(cls)\n\n if (classFilter && !classFilter(stable)) continue\n\n result.push(stable)\n }\n\n return result\n}\n\nexport function decomposeCSSModules(classes: string[]): CSSModuleClass[] {\n const result: CSSModuleClass[] = []\n for (const cls of classes) {\n const match = cls.match(CSS_MODULES_3SEG_RE)\n if (match) {\n result.push({\n raw: cls,\n moduleHint: match[1],\n localName: match[2],\n })\n }\n }\n return result\n}\n","import type { ElementFingerprint, WebRemarqOptions } from './types'\nimport { filterClasses, isHashedClass, decomposeCSSModules } from './hash-detect'\n\nconst TEXT_MAX_LENGTH = 50\n\nexport function createFingerprint(\n el: HTMLElement,\n options?: Pick<WebRemarqOptions, 'classFilter' | 'dataAttribute'>,\n): ElementFingerprint {\n const dataAttr = options?.dataAttribute ?? 'data-annotate'\n\n return {\n dataAnnotate: el.getAttribute(dataAttr) ?? null,\n dataTestId: el.getAttribute('data-testid')\n ?? el.getAttribute('data-test')\n ?? el.getAttribute('data-cy')\n ?? null,\n id: getStableId(el),\n tagName: el.tagName.toLowerCase(),\n textContent: getTextContent(el),\n role: el.getAttribute('role') ?? null,\n ariaLabel: el.getAttribute('aria-label') ?? null,\n stableClasses: filterClasses(\n Array.from(el.classList),\n options?.classFilter,\n ),\n domPath: buildDomPath(el),\n siblingIndex: getSiblingIndex(el),\n parentAnchor: findParentAnchor(el, dataAttr),\n rawClasses: Array.from(el.classList),\n cssModules: decomposeCSSModules(Array.from(el.classList)),\n }\n}\n\nfunction getStableId(el: HTMLElement): string | null {\n const id = el.id\n if (!id) return null\n if (isHashedClass(id)) return null\n return id\n}\n\nfunction getTextContent(el: HTMLElement): string | null {\n // Use only direct text nodes, not nested children's text\n let text = ''\n for (const node of Array.from(el.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n text += node.textContent ?? ''\n }\n }\n text = text.trim()\n\n // If no direct text, try first meaningful child's text (for wrappers like <span><b>Text</b></span>)\n if (!text && el.children.length <= 3) {\n text = el.textContent?.trim() ?? ''\n }\n\n if (!text) return null\n return text.length > TEXT_MAX_LENGTH ? text.slice(0, TEXT_MAX_LENGTH) : text\n}\n\nfunction buildDomPath(el: HTMLElement): string {\n const parts: string[] = []\n let current: HTMLElement | null = el\n\n while (current && current !== document.body && parts.length < 5) {\n let segment = current.tagName.toLowerCase()\n const stable = filterClasses(Array.from(current.classList))\n if (stable.length > 0) {\n segment += '.' + stable.slice(0, 2).join('.')\n }\n parts.unshift(segment)\n current = current.parentElement\n }\n\n return parts.join(' > ')\n}\n\nfunction getSiblingIndex(el: HTMLElement): number {\n const parent = el.parentElement\n if (!parent) return 0\n const children = Array.from(parent.children)\n return children.indexOf(el)\n}\n\nfunction findParentAnchor(el: HTMLElement, dataAttr: string): string | null {\n let current = el.parentElement\n while (current && current !== document.body) {\n const value = current.getAttribute(dataAttr)\n if (value) return value\n current = current.parentElement\n }\n return null\n}\n","import type { ElementFingerprint, WebRemarqOptions } from './types'\nimport { filterClasses } from './hash-detect'\n\nconst MATCH_THRESHOLD = 50\n\nexport function levenshteinSimilarity(a: string, b: string): number {\n if (a === b) return 1\n if (!a.length || !b.length) return 0\n\n const matrix: number[][] = []\n for (let i = 0; i <= a.length; i++) {\n matrix[i] = [i]\n }\n for (let j = 0; j <= b.length; j++) {\n matrix[0][j] = j\n }\n\n for (let i = 1; i <= a.length; i++) {\n for (let j = 1; j <= b.length; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n matrix[i][j] = Math.min(\n matrix[i - 1][j] + 1,\n matrix[i][j - 1] + 1,\n matrix[i - 1][j - 1] + cost,\n )\n }\n }\n\n const distance = matrix[a.length][b.length]\n return 1 - distance / Math.max(a.length, b.length)\n}\n\nfunction textSimilarity(a: string | null, b: string | null): number {\n if (!a || !b) return 0\n const na = a.trim().toLowerCase()\n const nb = b.trim().toLowerCase()\n if (na === nb) return 1\n if (na.includes(nb) || nb.includes(na)) return 1\n return levenshteinSimilarity(na, nb)\n}\n\nfunction jaccardSimilarity(a: string[], b: string[]): number {\n if (!a.length && !b.length) return 0\n const setA = new Set(a)\n const setB = new Set(b)\n let intersection = 0\n for (const item of setA) {\n if (setB.has(item)) intersection++\n }\n const union = new Set([...a, ...b]).size\n return union === 0 ? 0 : intersection / union\n}\n\nfunction scoreCandidate(el: HTMLElement, fp: ElementFingerprint, dataAttr: string): number {\n let score = 0\n\n // dataAnnotate match (+100)\n const elAnnotate = el.getAttribute(dataAttr)\n if (fp.dataAnnotate && elAnnotate === fp.dataAnnotate) {\n score += 100\n }\n\n // textContent match (+35 scaled)\n const elText = el.textContent?.trim().slice(0, 50) ?? null\n const textSim = textSimilarity(fp.textContent, elText)\n if (textSim > 0.7) {\n score += textSim * 35\n }\n\n // role + ariaLabel match (+30)\n if (fp.role && el.getAttribute('role') === fp.role &&\n fp.ariaLabel && el.getAttribute('aria-label') === fp.ariaLabel) {\n score += 30\n }\n\n // parentAnchor match (+15)\n if (fp.parentAnchor) {\n let parent = el.parentElement\n while (parent && parent !== document.body) {\n if (parent.getAttribute(dataAttr) === fp.parentAnchor) {\n score += 15\n break\n }\n parent = parent.parentElement\n }\n }\n\n // stableClasses overlap (+30 scaled)\n if (fp.stableClasses.length > 0) {\n const elClasses = filterClasses(Array.from(el.classList))\n const jaccard = jaccardSimilarity(fp.stableClasses, elClasses)\n score += jaccard * 30\n }\n\n // domPath match (+20 scaled)\n if (fp.domPath) {\n const elPath = buildDomPath(el)\n const pathSim = levenshteinSimilarity(fp.domPath, elPath)\n score += pathSim * 20\n }\n\n // siblingIndex match (+5)\n const parent = el.parentElement\n if (parent) {\n const idx = Array.from(parent.children).indexOf(el)\n if (idx === fp.siblingIndex) {\n score += 5\n }\n }\n\n return score\n}\n\nfunction buildDomPath(el: HTMLElement): string {\n const parts: string[] = []\n let current: HTMLElement | null = el\n while (current && current !== document.body && parts.length < 5) {\n let segment = current.tagName.toLowerCase()\n const stable = filterClasses(Array.from(current.classList))\n if (stable.length > 0) {\n segment += '.' + stable.slice(0, 2).join('.')\n }\n parts.unshift(segment)\n current = current.parentElement\n }\n return parts.join(' > ')\n}\n\nexport function matchElement(\n fp: ElementFingerprint,\n options?: Pick<WebRemarqOptions, 'dataAttribute'>,\n): HTMLElement | null {\n const dataAttr = options?.dataAttribute ?? 'data-annotate'\n\n // 1. Exact match by data-annotate\n if (fp.dataAnnotate) {\n const el = document.querySelector<HTMLElement>(`[${dataAttr}=\"${fp.dataAnnotate}\"]`)\n if (el) return el\n }\n\n // 2. Exact match by data-testid\n if (fp.dataTestId) {\n const el = document.querySelector<HTMLElement>(\n `[data-testid=\"${fp.dataTestId}\"], [data-test=\"${fp.dataTestId}\"], [data-cy=\"${fp.dataTestId}\"]`,\n )\n if (el) return el\n }\n\n // 3. Exact match by id\n if (fp.id) {\n const el = document.getElementById(fp.id) as HTMLElement | null\n if (el) return el\n }\n\n // 4. Fuzzy match by tagName + weighted scoring\n const candidates = document.querySelectorAll<HTMLElement>(fp.tagName)\n let bestEl: HTMLElement | null = null\n let bestScore = 0\n\n for (const candidate of candidates) {\n const score = scoreCandidate(candidate, fp, dataAttr)\n if (score > bestScore) {\n bestScore = score\n bestEl = candidate\n }\n }\n\n return bestScore >= MATCH_THRESHOLD ? bestEl : null\n}\n","type BucketChangeCallback = () => void\n\nlet currentBucket: number = 0\nlet onBucketChange: BucketChangeCallback | null = null\nlet resizeHandler: (() => void) | null = null\n\nexport function toBucket(width: number): number {\n return Math.floor(width / 100) * 100\n}\n\nfunction debounce(fn: () => void, ms: number): () => void {\n let timer: ReturnType<typeof setTimeout>\n return () => {\n clearTimeout(timer)\n timer = setTimeout(fn, ms)\n }\n}\n\nexport function initViewportListener(callback: BucketChangeCallback): void {\n currentBucket = toBucket(window.innerWidth)\n onBucketChange = callback\n\n resizeHandler = debounce(() => {\n const newBucket = toBucket(window.innerWidth)\n if (newBucket !== currentBucket) {\n currentBucket = newBucket\n onBucketChange?.()\n }\n }, 300)\n\n window.addEventListener('resize', resizeHandler)\n}\n\nexport function destroyViewportListener(): void {\n if (resizeHandler) {\n window.removeEventListener('resize', resizeHandler)\n resizeHandler = null\n }\n onBucketChange = null\n}\n","import type { Annotation, AnnotationStore } from './types'\nimport { toBucket } from './viewport'\n\nconst STORAGE_KEY = 'remarq:annotations'\n\nexport class AnnotationStorage {\n private annotations: Annotation[] = []\n private extraFields: Record<string, unknown> = {}\n isMemoryOnly = false\n\n constructor() {\n this.load()\n }\n\n getAll(): Annotation[] {\n return [...this.annotations]\n }\n\n getByRoute(route: string): Annotation[] {\n return this.annotations.filter((a) => a.route === route)\n }\n\n add(annotation: Annotation): void {\n this.annotations.push(annotation)\n this.save()\n }\n\n remove(id: string): void {\n this.annotations = this.annotations.filter((a) => a.id !== id)\n this.save()\n }\n\n update(id: string, changes: Partial<Annotation>): void {\n const idx = this.annotations.findIndex((a) => a.id === id)\n if (idx !== -1) {\n this.annotations[idx] = { ...this.annotations[idx], ...changes }\n this.save()\n }\n }\n\n clearAll(): void {\n this.annotations = []\n this.save()\n }\n\n exportJSON(): AnnotationStore {\n return {\n version: 1,\n annotations: [...this.annotations],\n }\n }\n\n importJSON(data: AnnotationStore): void {\n this.annotations = [...data.annotations]\n this.migrateViewportBuckets()\n this.save()\n }\n\n private migrateViewportBuckets(): void {\n for (const ann of this.annotations) {\n if (ann.viewportBucket == null && ann.viewport) {\n const width = parseInt(ann.viewport.split('x')[0], 10)\n ann.viewportBucket = toBucket(width)\n }\n }\n }\n\n private load(): void {\n try {\n const raw = localStorage.getItem(STORAGE_KEY)\n if (raw) {\n const parsed = JSON.parse(raw)\n const { version, annotations, ...rest } = parsed\n this.annotations = annotations ?? []\n this.extraFields = rest\n this.migrateViewportBuckets()\n }\n } catch {\n this.isMemoryOnly = true\n }\n }\n\n private save(): void {\n if (this.isMemoryOnly) return\n try {\n const data = {\n version: 1,\n ...this.extraFields,\n annotations: this.annotations,\n }\n localStorage.setItem(STORAGE_KEY, JSON.stringify(data))\n } catch {\n this.isMemoryOnly = true\n }\n }\n}\n"],"mappings":"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","names":["parent","buildDomPath"]}
package/dist/index.cjs CHANGED
@@ -171,6 +171,7 @@ var AnnotationStorage = class {
171
171
 
172
172
  // src/core/hash-detect.ts
173
173
  var CSS_MODULES_RE = /^(.+)__([a-zA-Z0-9]{3,})$/;
174
+ var CSS_MODULES_3SEG_RE = /^([^_]+(?:[_-][^_]+)*)__([a-zA-Z][a-zA-Z0-9]*)__([a-zA-Z0-9]{3,})$/;
174
175
  var STYLED_COMPONENTS_RE = /^sc-/;
175
176
  var EMOTION_RE = /^css-[a-zA-Z0-9]+$/;
176
177
  var PURE_HASH_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9]{8,}$/;
@@ -201,6 +202,20 @@ function filterClasses(classes, classFilter) {
201
202
  }
202
203
  return result;
203
204
  }
205
+ function decomposeCSSModules(classes) {
206
+ const result = [];
207
+ for (const cls of classes) {
208
+ const match = cls.match(CSS_MODULES_3SEG_RE);
209
+ if (match) {
210
+ result.push({
211
+ raw: cls,
212
+ moduleHint: match[1],
213
+ localName: match[2]
214
+ });
215
+ }
216
+ }
217
+ return result;
218
+ }
204
219
 
205
220
  // src/core/fingerprint.ts
206
221
  var TEXT_MAX_LENGTH = 50;
@@ -221,7 +236,9 @@ function createFingerprint(el, options2) {
221
236
  ),
222
237
  domPath: buildDomPath(el),
223
238
  siblingIndex: getSiblingIndex(el),
224
- parentAnchor: findParentAnchor(el, dataAttr)
239
+ parentAnchor: findParentAnchor(el, dataAttr),
240
+ rawClasses: Array.from(el.classList),
241
+ cssModules: decomposeCSSModules(Array.from(el.classList))
225
242
  };
226
243
  }
227
244
  function getStableId(el) {
@@ -789,6 +806,7 @@ var ThemeManager = class {
789
806
  // src/ui/toolbar.ts
790
807
  var ICONS = {
791
808
  inspect: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="7" cy="7" r="4"/><line x1="10" y1="10" x2="14" y2="14"/></svg>',
809
+ copy: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="8" height="9" rx="1"/><path d="M3 11V3a1 1 0 0 1 1-1h6"/></svg>',
792
810
  export: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v8M4 6l4-4 4 4M2 12h12"/></svg>',
793
811
  import: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 10V2M4 6l4 4 4-4M2 12h12"/></svg>',
794
812
  clear: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 4h10M6 4V3h4v1M5 4v9h6V4"/></svg>',
@@ -809,6 +827,7 @@ var Toolbar = class {
809
827
  this.badgeEl.className = "remarq-badge";
810
828
  this.badgeEl.style.display = "none";
811
829
  this.inspectBtn.appendChild(this.badgeEl);
830
+ const copyBtn = this.createButton("copy", ICONS.copy, () => callbacks.onCopy());
812
831
  const exportBtn = this.createButton("export", ICONS.export, (e) => this.toggleExportMenu(e));
813
832
  this.fileInput = document.createElement("input");
814
833
  this.fileInput.type = "file";
@@ -822,8 +841,9 @@ var Toolbar = class {
822
841
  const clearBtn = this.createButton("clear", ICONS.clear, () => callbacks.onClear());
823
842
  const themeBtn = this.createButton("theme", ICONS.theme, () => callbacks.onThemeToggle());
824
843
  const minimizeBtn = this.createButton("minimize", ICONS.minimize, () => this.toggleMinimize());
825
- this.buttons = [this.inspectBtn, exportBtn, importBtn, clearBtn, themeBtn];
844
+ this.buttons = [this.inspectBtn, copyBtn, exportBtn, importBtn, clearBtn, themeBtn];
826
845
  this.toolbarEl.appendChild(this.inspectBtn);
846
+ this.toolbarEl.appendChild(copyBtn);
827
847
  this.toolbarEl.appendChild(exportBtn);
828
848
  this.toolbarEl.appendChild(importBtn);
829
849
  this.toolbarEl.appendChild(clearBtn);
@@ -872,13 +892,13 @@ var Toolbar = class {
872
892
  this.exportMenu = document.createElement("div");
873
893
  this.exportMenu.className = "remarq-export-menu";
874
894
  const mdBtn = document.createElement("button");
875
- mdBtn.textContent = "Markdown (file)";
895
+ mdBtn.textContent = "MD (file)";
876
896
  mdBtn.addEventListener("click", () => {
877
897
  this.callbacks.onExportMd();
878
898
  this.closeExportMenu();
879
899
  });
880
900
  const jsonBtn = document.createElement("button");
881
- jsonBtn.textContent = "JSON (clipboard)";
901
+ jsonBtn.textContent = "JSON (file)";
882
902
  jsonBtn.addEventListener("click", () => {
883
903
  this.callbacks.onExportJson();
884
904
  this.closeExportMenu();
@@ -1544,37 +1564,78 @@ function handleMarkerClick(annotationId) {
1544
1564
  }
1545
1565
  );
1546
1566
  }
1547
- function exportMarkdown() {
1567
+ function generateMarkdown() {
1548
1568
  const route = currentRoute();
1549
1569
  const anns = storage.getByRoute(route);
1550
- if (!anns.length) return;
1570
+ if (!anns.length) return "";
1551
1571
  const lines = [`## Annotations \u2014 ${route} (${anns.length})`, ""];
1552
1572
  anns.forEach((ann, i) => {
1573
+ var _a, _b;
1553
1574
  const fp = ann.fingerprint;
1554
- let desc = `<${fp.tagName}>`;
1555
- if (fp.textContent) desc += ` "${fp.textContent}"`;
1556
- if (fp.parentAnchor) desc += ` (${fp.parentAnchor})`;
1557
- if (fp.dataAnnotate) desc += fp.parentAnchor ? ` > ${fp.dataAnnotate}` : ` (${fp.dataAnnotate})`;
1558
- const viewport = ann.viewport ? ` @${ann.viewport}` : "";
1559
- lines.push(`${i + 1}. [${ann.status}]${viewport} ${desc}: "${ann.comment}"`);
1575
+ lines.push(`### ${i + 1}. [${ann.status}] "${ann.comment}"`);
1576
+ let elDesc = `Element: <${fp.tagName}>`;
1577
+ if (fp.textContent) elDesc += ` "${fp.textContent}"`;
1578
+ lines.push(elDesc);
1579
+ lines.push(`Viewport: ${ann.viewportBucket}px`);
1580
+ lines.push("");
1581
+ lines.push("Search hints:");
1582
+ if (fp.dataAnnotate) {
1583
+ lines.push(`- \`data-annotate="${fp.dataAnnotate}"\` \u2014 in template files`);
1584
+ }
1585
+ if (fp.dataTestId) {
1586
+ lines.push(`- \`data-testid="${fp.dataTestId}"\` \u2014 in template files`);
1587
+ }
1588
+ if (fp.id) {
1589
+ lines.push(`- \`id="${fp.id}"\` \u2014 in template files`);
1590
+ }
1591
+ if (fp.ariaLabel) {
1592
+ lines.push(`- \`aria-label="${fp.ariaLabel}"\` \u2014 in template files`);
1593
+ }
1594
+ if (fp.textContent) {
1595
+ lines.push(`- \`"${fp.textContent}"\` \u2014 text content in templates`);
1596
+ }
1597
+ if ((_a = fp.cssModules) == null ? void 0 : _a.length) {
1598
+ for (const mod of fp.cssModules) {
1599
+ lines.push(`- \`.${mod.localName}\` \u2014 in CSS Module file (likely \`${mod.moduleHint}.module.*\`)`);
1600
+ lines.push(`- \`styles.${mod.localName}\` \u2014 in component JS/TS`);
1601
+ }
1602
+ }
1603
+ if (fp.domPath) {
1604
+ lines.push(`- DOM: ${fp.domPath}`);
1605
+ }
1606
+ const classes = (_b = fp.rawClasses) != null ? _b : fp.stableClasses;
1607
+ if (classes.length) {
1608
+ lines.push(`- Classes: ${classes.join(" ")}`);
1609
+ }
1610
+ lines.push("");
1560
1611
  });
1561
- const text = lines.join("\n");
1562
- const blob = new Blob([text], { type: "text/markdown" });
1612
+ return lines.join("\n");
1613
+ }
1614
+ function downloadFile(content, filename, type) {
1615
+ const blob = new Blob([content], { type });
1563
1616
  const url = URL.createObjectURL(blob);
1564
1617
  const a = document.createElement("a");
1565
1618
  a.href = url;
1566
- a.download = `remarq-annotations-${Date.now()}.md`;
1619
+ a.download = filename;
1567
1620
  a.click();
1568
1621
  URL.revokeObjectURL(url);
1569
1622
  }
1623
+ function exportMarkdown() {
1624
+ const md = generateMarkdown();
1625
+ if (!md) return;
1626
+ downloadFile(md, `remarq-annotations-${Date.now()}.md`, "text/markdown");
1627
+ }
1570
1628
  function exportJSON() {
1571
1629
  const data = storage.exportJSON();
1572
1630
  const json = JSON.stringify(data, null, 2);
1573
- try {
1574
- navigator.clipboard.writeText(json);
1575
- } catch (e) {
1631
+ downloadFile(json, `remarq-annotations-${Date.now()}.json`, "application/json");
1632
+ }
1633
+ function copyToClipboard() {
1634
+ const md = generateMarkdown();
1635
+ if (!md) return;
1636
+ navigator.clipboard.writeText(md).catch(() => {
1576
1637
  console.warn("[web-remarq] Clipboard write failed");
1577
- }
1638
+ });
1578
1639
  }
1579
1640
  function setupMutationObserver() {
1580
1641
  mutationObserver = new MutationObserver((mutations) => {
@@ -1608,6 +1669,7 @@ var WebRemarq = {
1608
1669
  });
1609
1670
  toolbar = new Toolbar(themeManager.container, {
1610
1671
  onInspect: () => setInspecting(!inspecting),
1672
+ onCopy: copyToClipboard,
1611
1673
  onExportMd: exportMarkdown,
1612
1674
  onExportJson: exportJSON,
1613
1675
  onImport: () => {
@@ -1673,6 +1735,9 @@ var WebRemarq = {
1673
1735
  if (format === "md") exportMarkdown();
1674
1736
  else exportJSON();
1675
1737
  },
1738
+ copy() {
1739
+ copyToClipboard();
1740
+ },
1676
1741
  async import(file) {
1677
1742
  const text = await file.text();
1678
1743
  const data = JSON.parse(text);