tftsr-devops_investigation/node_modules/mdast-util-to-markdown/lib/util/safe.js
Shaun Arman 8839075805 feat: initial implementation of TFTSR IT Triage & RCA application
Implements Phases 1-8 of the TFTSR implementation plan.

Rust backend (Tauri 2.x, src-tauri/):
- Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama
- PII detection engine: 11 regex patterns with overlap resolution
- SQLCipher AES-256 encrypted database with 10 versioned migrations
- 28 Tauri IPC commands for triage, analysis, document, and system ops
- Ollama: hardware probe, model recommendations, pull/delete with events
- RCA and blameless post-mortem Markdown document generators
- PDF export via printpdf
- Audit log: SHA-256 hash of every external data send
- Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2)

Frontend (React 18 + TypeScript + Vite, src/):
- 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings
- 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives
- 3 Zustand stores: session, settings (persisted), history
- Type-safe tauriCommands.ts matching Rust backend types exactly
- 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs)

DevOps:
- .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push
- .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload

Verified:
- cargo check: zero errors
- tsc --noEmit: zero errors
- vitest run: 13/13 unit tests passing

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 22:36:25 -05:00

175 lines
4.5 KiB
JavaScript

/**
* @import {SafeConfig, State} from 'mdast-util-to-markdown'
*/
import {encodeCharacterReference} from './encode-character-reference.js'
import {patternInScope} from './pattern-in-scope.js'
/**
* Make a string safe for embedding in markdown constructs.
*
* In markdown, almost all punctuation characters can, in certain cases,
* result in something.
* Whether they do is highly subjective to where they happen and in what
* they happen.
*
* To solve this, `mdast-util-to-markdown` tracks:
*
* * Characters before and after something;
* * What “constructs” we are in.
*
* This information is then used by this function to escape or encode
* special characters.
*
* @param {State} state
* Info passed around about the current state.
* @param {string | null | undefined} input
* Raw value to make safe.
* @param {SafeConfig} config
* Configuration.
* @returns {string}
* Serialized markdown safe for embedding.
*/
export function safe(state, input, config) {
const value = (config.before || '') + (input || '') + (config.after || '')
/** @type {Array<number>} */
const positions = []
/** @type {Array<string>} */
const result = []
/** @type {Record<number, {before: boolean, after: boolean}>} */
const infos = {}
let index = -1
while (++index < state.unsafe.length) {
const pattern = state.unsafe[index]
if (!patternInScope(state.stack, pattern)) {
continue
}
const expression = state.compilePattern(pattern)
/** @type {RegExpExecArray | null} */
let match
while ((match = expression.exec(value))) {
const before = 'before' in pattern || Boolean(pattern.atBreak)
const after = 'after' in pattern
const position = match.index + (before ? match[1].length : 0)
if (positions.includes(position)) {
if (infos[position].before && !before) {
infos[position].before = false
}
if (infos[position].after && !after) {
infos[position].after = false
}
} else {
positions.push(position)
infos[position] = {before, after}
}
}
}
positions.sort(numerical)
let start = config.before ? config.before.length : 0
const end = value.length - (config.after ? config.after.length : 0)
index = -1
while (++index < positions.length) {
const position = positions[index]
// Character before or after matched:
if (position < start || position >= end) {
continue
}
// If this character is supposed to be escaped because it has a condition on
// the next character, and the next character is definitly being escaped,
// then skip this escape.
if (
(position + 1 < end &&
positions[index + 1] === position + 1 &&
infos[position].after &&
!infos[position + 1].before &&
!infos[position + 1].after) ||
(positions[index - 1] === position - 1 &&
infos[position].before &&
!infos[position - 1].before &&
!infos[position - 1].after)
) {
continue
}
if (start !== position) {
// If we have to use a character reference, an ampersand would be more
// correct, but as backslashes only care about punctuation, either will
// do the trick
result.push(escapeBackslashes(value.slice(start, position), '\\'))
}
start = position
if (
/[!-/:-@[-`{-~]/.test(value.charAt(position)) &&
(!config.encode || !config.encode.includes(value.charAt(position)))
) {
// Character escape.
result.push('\\')
} else {
// Character reference.
result.push(encodeCharacterReference(value.charCodeAt(position)))
start++
}
}
result.push(escapeBackslashes(value.slice(start, end), config.after))
return result.join('')
}
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
function numerical(a, b) {
return a - b
}
/**
* @param {string} value
* @param {string} after
* @returns {string}
*/
function escapeBackslashes(value, after) {
const expression = /\\(?=[!-/:-@[-`{-~])/g
/** @type {Array<number>} */
const positions = []
/** @type {Array<string>} */
const results = []
const whole = value + after
let index = -1
let start = 0
/** @type {RegExpExecArray | null} */
let match
while ((match = expression.exec(whole))) {
positions.push(match.index)
}
while (++index < positions.length) {
if (start !== positions[index]) {
results.push(value.slice(start, positions[index]))
}
results.push('\\')
start = positions[index]
}
results.push(value.slice(start))
return results.join('')
}