Stabilize pro editing without heavy office runtime
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>
This commit is contained in:
162
src/utils/hiddenText.js
Normal file
162
src/utils/hiddenText.js
Normal file
@@ -0,0 +1,162 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user