The workspace now carries the pro editing flow, streaming completion path, and lighter Office preview state as one checkpoint so the remote has the current runnable project shape. Constraint: Preserve the current workspace as a single reviewable project commit while excluding local agent state and verification artifacts. Removed stale Univer runtime dependencies from the lockfile so installs match package.json. Rejected: Commit runtime screenshots, .omx state, and coverage files | they are local artifacts rather than source state. Confidence: medium Scope-risk: broad Directive: Keep package.json and package-lock.json synchronized when changing frontend dependencies. Tested: npm run build; C:\Users\ydy\.conda\envs\llmwebsite\python.exe -m pytest backend/tests/test_main_endpoints.py backend/tests/test_main_cancel.py backend/tests/test_llm.py backend/tests/test_llm_extended.py -v -o addopts= (44 passed). Not-tested: Full pytest with repository coverage addopts currently reports 0% coverage because pytest-cov watches backend.* module names while tests import top-level backend modules. Co-authored-by: OmX <omx@oh-my-codex.dev>
162 lines
3.8 KiB
JavaScript
162 lines
3.8 KiB
JavaScript
export const HIDDEN_TEXT_NODE_TYPE = 'hiddenText'
|
|
|
|
function escapeHtml(value = '') {
|
|
return String(value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
}
|
|
|
|
export function normalizeHiddenTextValue(value = '') {
|
|
return String(value ?? '').replace(/\r\n?/g, ' ')
|
|
}
|
|
|
|
export function escapeHiddenTextSegment(value = '', closingChar) {
|
|
const normalized = normalizeHiddenTextValue(value)
|
|
const closingCharPattern = new RegExp(`\\${closingChar}`, 'g')
|
|
return normalized
|
|
.replace(/\\/g, '\\\\')
|
|
.replace(closingCharPattern, `\\${closingChar}`)
|
|
}
|
|
|
|
export function serializeHiddenTextSyntax(displayed = '', hidden = '') {
|
|
const safeDisplayed = escapeHiddenTextSegment(displayed, ')')
|
|
const safeHidden = escapeHiddenTextSegment(hidden, '}')
|
|
return `(${safeDisplayed}){${safeHidden}}`
|
|
}
|
|
|
|
function readHiddenTextSegment(text = '', start = 0, closingChar = ')') {
|
|
let index = start
|
|
let value = ''
|
|
|
|
while (index < text.length) {
|
|
const char = text[index]
|
|
|
|
if (char === '\\') {
|
|
const nextChar = text[index + 1]
|
|
if (nextChar === undefined) return null
|
|
value += nextChar
|
|
index += 2
|
|
continue
|
|
}
|
|
|
|
if (char === '\n' || char === '\r') return null
|
|
if (char === closingChar) {
|
|
return {
|
|
value,
|
|
end: index + 1,
|
|
}
|
|
}
|
|
|
|
value += char
|
|
index += 1
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function parseHiddenTextAt(text = '', start = 0) {
|
|
if (text[start] !== '(') return null
|
|
|
|
const displayed = readHiddenTextSegment(text, start + 1, ')')
|
|
if (!displayed) return null
|
|
if (text[displayed.end] !== '{') return null
|
|
|
|
const hidden = readHiddenTextSegment(text, displayed.end + 1, '}')
|
|
if (!hidden) return null
|
|
|
|
return {
|
|
start,
|
|
end: hidden.end,
|
|
displayed: displayed.value,
|
|
hidden: hidden.value,
|
|
raw: text.slice(start, hidden.end),
|
|
}
|
|
}
|
|
|
|
export function extractHiddenTextMatches(text = '') {
|
|
const matches = []
|
|
let index = 0
|
|
|
|
while (index < text.length) {
|
|
const match = parseHiddenTextAt(text, index)
|
|
if (match) {
|
|
matches.push(match)
|
|
index = match.end
|
|
continue
|
|
}
|
|
|
|
index += 1
|
|
}
|
|
|
|
return matches
|
|
}
|
|
|
|
export function splitTextWithHiddenSyntax(text = '') {
|
|
const matches = extractHiddenTextMatches(text)
|
|
if (matches.length === 0) return null
|
|
|
|
const segments = []
|
|
let cursor = 0
|
|
|
|
for (const match of matches) {
|
|
if (match.start > cursor) {
|
|
segments.push({
|
|
type: 'text',
|
|
value: text.slice(cursor, match.start),
|
|
})
|
|
}
|
|
|
|
segments.push({
|
|
type: HIDDEN_TEXT_NODE_TYPE,
|
|
displayed: match.displayed,
|
|
hidden: match.hidden,
|
|
})
|
|
|
|
cursor = match.end
|
|
}
|
|
|
|
if (cursor < text.length) {
|
|
segments.push({
|
|
type: 'text',
|
|
value: text.slice(cursor),
|
|
})
|
|
}
|
|
|
|
return segments.filter((segment) => segment.type !== 'text' || segment.value)
|
|
}
|
|
|
|
export function renderHiddenTextPreviewHtml(displayed = '', hidden = '') {
|
|
const summary = escapeHtml(displayed || '未命名文本')
|
|
|
|
return [
|
|
`<span class="hidden-text-preview" data-hidden-text="true" title="${escapeHtml(serializeHiddenTextSyntax(displayed, hidden))}">`,
|
|
`<span class="hidden-text-preview__summary">${summary}</span>`,
|
|
'</span>',
|
|
].join('')
|
|
}
|
|
|
|
export function hiddenTextMarkdownItPlugin(md) {
|
|
md.inline.ruler.before('emphasis', 'hidden_text', (state, silent) => {
|
|
const match = parseHiddenTextAt(state.src, state.pos)
|
|
if (!match) return false
|
|
|
|
if (!silent) {
|
|
const token = state.push('hidden_text', '', 0)
|
|
token.meta = {
|
|
displayed: match.displayed,
|
|
hidden: match.hidden,
|
|
}
|
|
}
|
|
|
|
state.pos = match.end
|
|
return true
|
|
})
|
|
|
|
md.renderer.rules.hidden_text = (tokens, index) => {
|
|
const meta = tokens[index]?.meta || {}
|
|
return renderHiddenTextPreviewHtml(meta.displayed, meta.hidden)
|
|
}
|
|
} |