Files
Starlight_Lancher/packages/server/src/properties.ts

142 lines
3.6 KiB
TypeScript

export type PropertiesEntry =
| { type: 'comment'; text: string }
| { type: 'blank' }
| { type: 'pair'; key: string; value: string; separator: string }
const COMMENT_RE = /^\s*[#!]/
const ESCAPE_RE = /\\(.)/g
function unescapeValue(value: string): string {
return value.replace(ESCAPE_RE, (_, char: string) => {
switch (char) {
case 'n':
return '\n'
case 't':
return '\t'
case 'r':
return '\r'
case 'f':
return '\f'
default:
return char
}
})
}
function escapeValue(value: string): string {
return value
.replace(/\\/g, '\\\\')
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t')
.replace(/\r/g, '\\r')
}
/** Finds the first unescaped `=` or `:` separator, falling back to whitespace. */
function findSeparator(line: string): number {
for (let i = 0; i < line.length; i++) {
const char = line[i]
if (char === '\\') {
i++
continue
}
if (char === '=' || char === ':') return i
if (/\s/.test(char)) {
let j = i
while (j < line.length && /\s/.test(line[j])) j++
if (line[j] === '=' || line[j] === ':') return j
return i
}
}
return -1
}
function unescapeKey(key: string): string {
return unescapeValue(key).replace(/\\([ =:])/g, '$1')
}
/**
* Parses a Java `.properties` document, preserving comment lines, blank lines,
* key order, and the original `=`/`:` separators so it round-trips safely.
*/
export function parseProperties(text: string): PropertiesEntry[] {
const entries: PropertiesEntry[] = []
let pendingContinuation: { type: 'pair'; key: string; value: string; separator: string } | null =
null
for (const rawLine of text.split(/\r?\n/)) {
const line = pendingContinuation ? rawLine : rawLine.trim()
if (pendingContinuation) {
const continued = line.replace(/\\\s*$/, '')
pendingContinuation.value += continued
if (/\\\s*$/.test(rawLine)) continue
entries.push(pendingContinuation)
pendingContinuation = null
continue
}
if (line === '') {
entries.push({ type: 'blank' })
continue
}
if (COMMENT_RE.test(line)) {
entries.push({ type: 'comment', text: line })
continue
}
const separatorIndex = findSeparator(line)
if (separatorIndex === -1) {
entries.push({ type: 'pair', key: unescapeKey(line), value: '', separator: '=' })
continue
}
const key = unescapeKey(line.slice(0, separatorIndex).trimEnd())
const separator = line[separatorIndex]
const value = line.slice(separatorIndex + 1).trim()
if (/\\\s*$/.test(value)) {
pendingContinuation = {
type: 'pair',
key,
value: value.replace(/\\\s*$/, ''),
separator,
}
continue
}
entries.push({ type: 'pair', key, value: unescapeValue(value), separator })
}
if (pendingContinuation) entries.push(pendingContinuation)
return entries
}
export function serializeProperties(entries: PropertiesEntry[]): string {
return entries
.map((entry) => {
if (entry.type === 'blank') return ''
if (entry.type === 'comment') return entry.text
return `${entry.key.replace(/([ =:])/g, '\\$1')}${entry.separator}${escapeValue(entry.value)}`
})
.join('\n')
}
export function getProperty(entries: PropertiesEntry[], key: string): string | undefined {
const entry = entries.find(
(entry): entry is Extract<PropertiesEntry, { type: 'pair' }> =>
entry.type === 'pair' && entry.key === key,
)
return entry?.value
}
export function setProperty(
entries: PropertiesEntry[],
key: string,
value: string,
): PropertiesEntry[] {
let updated = false
const next = entries.map((entry) => {
if (entry.type === 'pair' && entry.key === key) {
updated = true
return { ...entry, value }
}
return entry
})
if (!updated) next.push({ type: 'pair', key, value, separator: '=' })
return next
}