feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
/**
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
* Content Extractor
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
*
|
|
|
|
|
* Detects content type (plaintext, rich-text JSON, HTML, Markdown) and extracts
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
* text segments with content categories (title, content, code, etc.).
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
*
|
|
|
|
|
* Supports common rich-text editor formats:
|
|
|
|
|
* - TipTap / ProseMirror: { type: 'doc', content: [...] }
|
|
|
|
|
* - Slate.js: [{ type, children: [{ text }] }]
|
|
|
|
|
* - Lexical: { root: { children: [...] } }
|
|
|
|
|
* - Draft.js: { blocks: [{ text }] }
|
|
|
|
|
* - Quill Delta: { ops: [{ insert }] }
|
|
|
|
|
*
|
|
|
|
|
* Falls back gracefully: structured text that doesn't match known patterns
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
* is extracted as plain content via recursive text collection.
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import type { ContentType, ContentCategory, ExtractedSegment } from '../types/brainy.types.js'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Detect content type from text content (no filename needed)
|
|
|
|
|
*/
|
|
|
|
|
export function detectContentType(text: string): ContentType {
|
|
|
|
|
const trimmed = text.trimStart()
|
|
|
|
|
|
|
|
|
|
// JSON detection (covers TipTap, Slate, Lexical, Draft.js, Quill, etc.)
|
|
|
|
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
|
|
|
try {
|
|
|
|
|
JSON.parse(trimmed)
|
|
|
|
|
return 'richtext-json'
|
|
|
|
|
} catch {
|
|
|
|
|
// Not valid JSON, fall through
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HTML detection (tag at start of content)
|
|
|
|
|
if (/<[a-z!][a-z0-9]*[\s>/]/i.test(trimmed.substring(0, 200))) {
|
|
|
|
|
return 'html'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Markdown detection (heading or code fence at start)
|
|
|
|
|
if (/^#{1,6}\s/m.test(trimmed.substring(0, 500)) ||
|
|
|
|
|
/^```/m.test(trimmed.substring(0, 500))) {
|
|
|
|
|
return 'markdown'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'plaintext'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract text segments from content, auto-detecting type if not provided
|
|
|
|
|
*/
|
|
|
|
|
export function extractForHighlighting(
|
|
|
|
|
text: string,
|
|
|
|
|
contentType?: ContentType
|
|
|
|
|
): ExtractedSegment[] {
|
|
|
|
|
const type = contentType || detectContentType(text)
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'richtext-json':
|
|
|
|
|
return extractFromJson(text)
|
|
|
|
|
case 'html':
|
|
|
|
|
return extractFromHtml(text)
|
|
|
|
|
case 'markdown':
|
|
|
|
|
return extractFromMarkdown(text)
|
|
|
|
|
case 'plaintext':
|
|
|
|
|
default:
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
return [{ text, contentCategory: 'content' }]
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============= Rich-Text JSON Extraction =============
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract text from any JSON rich-text format.
|
|
|
|
|
* Walks the node tree looking for text leaves across all common editors.
|
|
|
|
|
*/
|
|
|
|
|
function extractFromJson(text: string): ExtractedSegment[] {
|
|
|
|
|
let parsed: any
|
|
|
|
|
try {
|
|
|
|
|
parsed = JSON.parse(text.trim())
|
|
|
|
|
} catch {
|
|
|
|
|
// Invalid JSON — treat as plain text
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
return [{ text, contentCategory: 'content' }]
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const segments = walkRichTextNodes(parsed)
|
|
|
|
|
|
|
|
|
|
// If the walker found real text segments, return them
|
|
|
|
|
if (segments.length > 0 && segments.some(s => s.text.trim().length > 0)) {
|
|
|
|
|
return segments.filter(s => s.text.trim().length > 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback: collect all string values from the JSON
|
|
|
|
|
const fallbackText = extractTextFromJsonValue(parsed)
|
|
|
|
|
if (fallbackText.trim()) {
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
return [{ text: fallbackText, contentCategory: 'content' }]
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Walk rich-text nodes recursively.
|
|
|
|
|
* Handles TipTap/ProseMirror, Slate, Lexical, Draft.js, and Quill Delta.
|
|
|
|
|
*/
|
|
|
|
|
function walkRichTextNodes(node: any): ExtractedSegment[] {
|
|
|
|
|
if (node === null || node === undefined) return []
|
|
|
|
|
|
|
|
|
|
const segments: ExtractedSegment[] = []
|
|
|
|
|
|
|
|
|
|
// Handle arrays (Slate root is an array, Draft.js blocks, Quill ops)
|
|
|
|
|
if (Array.isArray(node)) {
|
|
|
|
|
for (const child of node) {
|
|
|
|
|
segments.push(...walkRichTextNodes(child))
|
|
|
|
|
}
|
|
|
|
|
return segments
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof node !== 'object') return []
|
|
|
|
|
|
|
|
|
|
// Leaf: text content (TipTap/ProseMirror/Lexical/Slate)
|
|
|
|
|
if (typeof node.text === 'string' && node.text.length > 0) {
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
segments.push({ text: node.text, contentCategory: 'content' })
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
return segments
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Leaf: Quill Delta insert
|
|
|
|
|
if (typeof node.insert === 'string' && node.insert.length > 0) {
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
segments.push({ text: node.insert, contentCategory: 'content' })
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
return segments
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draft.js block with text
|
|
|
|
|
if (typeof node.text === 'string' && node.type !== undefined && node.text.length > 0) {
|
|
|
|
|
const category = categorizeNodeType(node.type)
|
|
|
|
|
segments.push({ text: node.text, contentCategory: category })
|
|
|
|
|
return segments
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Categorize by node type
|
|
|
|
|
const category = categorizeNodeType(node.type)
|
|
|
|
|
|
|
|
|
|
// Walk children arrays: content (TipTap), children (Slate/Lexical), blocks (Draft.js), ops (Quill)
|
|
|
|
|
const children = node.content || node.children || node.blocks || node.ops
|
|
|
|
|
if (Array.isArray(children)) {
|
|
|
|
|
for (const child of children) {
|
|
|
|
|
const childSegments = walkRichTextNodes(child)
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
// Apply parent's category if it's more specific than 'content'
|
|
|
|
|
if (category !== 'content') {
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
childSegments.forEach(s => { s.contentCategory = category })
|
|
|
|
|
}
|
|
|
|
|
segments.push(...childSegments)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Lexical root wrapper
|
|
|
|
|
if (node.root && typeof node.root === 'object') {
|
|
|
|
|
segments.push(...walkRichTextNodes(node.root))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return segments
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Categorize a node type string into a ContentCategory
|
|
|
|
|
*/
|
|
|
|
|
function categorizeNodeType(type?: string): ContentCategory {
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
if (!type) return 'content'
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
const t = type.toLowerCase()
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
if (t === 'heading' || /^h[1-6]$/.test(t)) return 'title'
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
if (t === 'code' || t === 'codeblock' || t === 'code_block') return 'code'
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
return 'content'
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fallback: recursively extract all string values from JSON
|
|
|
|
|
*/
|
|
|
|
|
function extractTextFromJsonValue(value: any): string {
|
|
|
|
|
if (typeof value === 'string') return value
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
return value.map(extractTextFromJsonValue).filter(Boolean).join(' ')
|
|
|
|
|
}
|
|
|
|
|
if (typeof value === 'object' && value !== null) {
|
|
|
|
|
const texts: string[] = []
|
|
|
|
|
for (const v of Object.values(value)) {
|
|
|
|
|
const text = extractTextFromJsonValue(v)
|
|
|
|
|
if (text) texts.push(text)
|
|
|
|
|
}
|
|
|
|
|
return texts.join(' ')
|
|
|
|
|
}
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============= HTML Extraction =============
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract text from HTML using a simple state-machine tag parser.
|
|
|
|
|
* No external dependencies.
|
|
|
|
|
*/
|
|
|
|
|
function extractFromHtml(html: string): ExtractedSegment[] {
|
|
|
|
|
const segments: ExtractedSegment[] = []
|
|
|
|
|
let current = ''
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
let currentCategory: ContentCategory = 'content'
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
let i = 0
|
|
|
|
|
let insideTag = false
|
|
|
|
|
let tagName = ''
|
|
|
|
|
let isClosingTag = false
|
|
|
|
|
let skipContent = false
|
|
|
|
|
|
|
|
|
|
// Tag stack to track nesting
|
|
|
|
|
const tagStack: string[] = []
|
|
|
|
|
|
|
|
|
|
while (i < html.length) {
|
|
|
|
|
if (html[i] === '<') {
|
|
|
|
|
// Flush current text if we have any
|
|
|
|
|
if (current.trim() && !skipContent) {
|
|
|
|
|
segments.push({ text: current.trim(), contentCategory: currentCategory })
|
|
|
|
|
}
|
|
|
|
|
current = ''
|
|
|
|
|
|
|
|
|
|
insideTag = true
|
|
|
|
|
tagName = ''
|
|
|
|
|
isClosingTag = false
|
|
|
|
|
i++
|
|
|
|
|
|
|
|
|
|
// Check for closing tag
|
|
|
|
|
if (i < html.length && html[i] === '/') {
|
|
|
|
|
isClosingTag = true
|
|
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read tag name
|
|
|
|
|
while (i < html.length && html[i] !== '>' && html[i] !== ' ' && html[i] !== '/') {
|
|
|
|
|
tagName += html[i]
|
|
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip to end of tag
|
|
|
|
|
while (i < html.length && html[i] !== '>') {
|
|
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
if (i < html.length) i++ // skip '>'
|
|
|
|
|
|
|
|
|
|
const tagLower = tagName.toLowerCase()
|
|
|
|
|
|
|
|
|
|
if (isClosingTag) {
|
|
|
|
|
// Pop tag stack
|
|
|
|
|
if (tagStack.length > 0 && tagStack[tagStack.length - 1] === tagLower) {
|
|
|
|
|
tagStack.pop()
|
|
|
|
|
}
|
|
|
|
|
if (tagLower === 'script' || tagLower === 'style') {
|
|
|
|
|
skipContent = false
|
|
|
|
|
}
|
|
|
|
|
// Reset category based on remaining stack
|
|
|
|
|
currentCategory = getCategoryFromTagStack(tagStack)
|
|
|
|
|
} else {
|
|
|
|
|
// Opening tag
|
|
|
|
|
if (tagLower === 'script' || tagLower === 'style') {
|
|
|
|
|
skipContent = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Self-closing tags don't affect stack
|
|
|
|
|
const selfClosing = html[i - 2] === '/' ||
|
|
|
|
|
['br', 'hr', 'img', 'input', 'meta', 'link'].includes(tagLower)
|
|
|
|
|
|
|
|
|
|
if (!selfClosing) {
|
|
|
|
|
tagStack.push(tagLower)
|
|
|
|
|
currentCategory = getCategoryFromTagStack(tagStack)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
insideTag = false
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!insideTag) {
|
|
|
|
|
// Decode common HTML entities
|
|
|
|
|
if (html[i] === '&') {
|
|
|
|
|
const entityEnd = html.indexOf(';', i)
|
|
|
|
|
if (entityEnd !== -1 && entityEnd - i < 10) {
|
|
|
|
|
const entity = html.substring(i, entityEnd + 1)
|
|
|
|
|
current += decodeHtmlEntity(entity)
|
|
|
|
|
i = entityEnd + 1
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
current += html[i]
|
|
|
|
|
}
|
|
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Flush remaining text
|
|
|
|
|
if (current.trim() && !skipContent) {
|
|
|
|
|
segments.push({ text: current.trim(), contentCategory: currentCategory })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return segments.filter(s => s.text.length > 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determine content category from the current tag stack
|
|
|
|
|
*/
|
|
|
|
|
function getCategoryFromTagStack(stack: string[]): ContentCategory {
|
|
|
|
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
|
|
|
const tag = stack[i]
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
if (/^h[1-6]$/.test(tag)) return 'title'
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
if (tag === 'code' || tag === 'pre') return 'code'
|
|
|
|
|
}
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
return 'content'
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Decode common HTML entities
|
|
|
|
|
*/
|
|
|
|
|
function decodeHtmlEntity(entity: string): string {
|
|
|
|
|
const entities: Record<string, string> = {
|
|
|
|
|
'&': '&',
|
|
|
|
|
'<': '<',
|
|
|
|
|
'>': '>',
|
|
|
|
|
'"': '"',
|
|
|
|
|
''': "'",
|
|
|
|
|
''': "'",
|
|
|
|
|
' ': ' ',
|
|
|
|
|
}
|
|
|
|
|
return entities[entity] || entity
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============= Markdown Extraction =============
|
|
|
|
|
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
/**
|
|
|
|
|
* Split a prose line into alternating content/code segments based on
|
|
|
|
|
* backtick-delimited inline code spans. Lines without backticks return
|
|
|
|
|
* a single 'content' segment.
|
|
|
|
|
*/
|
|
|
|
|
function splitInlineCode(line: string): ExtractedSegment[] {
|
|
|
|
|
const segments: ExtractedSegment[] = []
|
|
|
|
|
const pattern = /`([^`]+)`/g
|
|
|
|
|
let lastIndex = 0
|
|
|
|
|
let match: RegExpExecArray | null
|
|
|
|
|
|
|
|
|
|
while ((match = pattern.exec(line)) !== null) {
|
|
|
|
|
// Text before the backtick span
|
|
|
|
|
if (match.index > lastIndex) {
|
|
|
|
|
const before = line.substring(lastIndex, match.index).trim()
|
|
|
|
|
if (before.length > 0) {
|
|
|
|
|
segments.push({ text: before, contentCategory: 'content' })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// The code span (without backticks)
|
|
|
|
|
segments.push({ text: match[1], contentCategory: 'code' })
|
|
|
|
|
lastIndex = match.index + match[0].length
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remaining text after last backtick span (or entire line if no backticks)
|
|
|
|
|
if (lastIndex < line.length) {
|
|
|
|
|
const remaining = line.substring(lastIndex).trim()
|
|
|
|
|
if (remaining.length > 0) {
|
|
|
|
|
segments.push({ text: remaining, contentCategory: 'content' })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return segments
|
|
|
|
|
}
|
|
|
|
|
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
/**
|
|
|
|
|
* Extract text from Markdown with category detection
|
|
|
|
|
*/
|
|
|
|
|
function extractFromMarkdown(text: string): ExtractedSegment[] {
|
|
|
|
|
const segments: ExtractedSegment[] = []
|
|
|
|
|
const lines = text.split('\n')
|
|
|
|
|
let inCodeBlock = false
|
|
|
|
|
let codeContent = ''
|
|
|
|
|
let i = 0
|
|
|
|
|
|
|
|
|
|
while (i < lines.length) {
|
|
|
|
|
const line = lines[i]
|
|
|
|
|
|
|
|
|
|
// Fenced code block toggle
|
|
|
|
|
if (/^```/.test(line.trimStart())) {
|
|
|
|
|
if (inCodeBlock) {
|
|
|
|
|
// End of code block
|
|
|
|
|
if (codeContent.trim()) {
|
|
|
|
|
segments.push({ text: codeContent.trim(), contentCategory: 'code' })
|
|
|
|
|
}
|
|
|
|
|
codeContent = ''
|
|
|
|
|
inCodeBlock = false
|
|
|
|
|
} else {
|
|
|
|
|
// Start of code block
|
|
|
|
|
inCodeBlock = true
|
|
|
|
|
}
|
|
|
|
|
i++
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (inCodeBlock) {
|
|
|
|
|
codeContent += (codeContent ? '\n' : '') + line
|
|
|
|
|
i++
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Indented code block (4 spaces or 1 tab)
|
|
|
|
|
if (/^(?: |\t)/.test(line) && line.trim().length > 0) {
|
|
|
|
|
let codeLines = line.replace(/^(?: |\t)/, '')
|
|
|
|
|
i++
|
|
|
|
|
while (i < lines.length && (/^(?: |\t)/.test(lines[i]) || lines[i].trim() === '')) {
|
|
|
|
|
codeLines += '\n' + lines[i].replace(/^(?: |\t)/, '')
|
|
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
if (codeLines.trim()) {
|
|
|
|
|
segments.push({ text: codeLines.trim(), contentCategory: 'code' })
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Heading (# style)
|
|
|
|
|
const headingMatch = line.match(/^#{1,6}\s+(.+)$/)
|
|
|
|
|
if (headingMatch) {
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
segments.push({ text: headingMatch[1].trim(), contentCategory: 'title' })
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
i++
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
// Regular content line — split out inline code spans
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
if (line.trim().length > 0) {
|
feat: expand ContentCategory to universal 6-category set for highlight()
Replace document-centric categories (prose/heading/code/label) with a
universal set that works across documents, code, and UI:
- title: headings, identifiers, labels, JSON keys
- annotation: comments, docstrings, captions, alt text
- content: paragraphs, list items, flowing text
- value: string literals, numbers, form values
- code: unparsed code blocks
- structural: keywords, operators, punctuation
Built-in extractors now produce title/content/code. All 6 categories
are available for custom parsers (e.g. tree-sitter).
Also adds inline code detection in Markdown: backtick spans within
prose lines are split into separate code/content segments.
2026-01-27 13:44:58 -08:00
|
|
|
segments.push(...splitInlineCode(line.trim()))
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Flush unclosed code block
|
|
|
|
|
if (inCodeBlock && codeContent.trim()) {
|
|
|
|
|
segments.push({ text: codeContent.trim(), contentCategory: 'code' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return segments.filter(s => s.text.length > 0)
|
|
|
|
|
}
|