Files
plainleaf/plug-api/lib/yaml.ts
T

182 lines
6.0 KiB
TypeScript

// Define the patch structure
export interface SetKeyPatch {
op: "set-key";
path: string; // Still assuming simple, top-level key names
value: any; // Allow any value, including arrays
}
// Helper function specifically for serializing scalar types
function serializeToYamlScalar(
value: string | number | boolean | null,
): string {
if (typeof value === "string") {
// Always quote empty strings and strings with special characters
if (
value === "" || // Empty string
value.match(/[:{#}[],&*!|>'"%@`]/) || // Special YAML characters (added % and ")
/^\d+(\.\d+)?([eE][+-]?\d+)?$/.test(value) || // Looks like a number
value.includes(":") || // Contains colons
["true", "false", "null", "yes", "no", "on", "off"].includes(
value.toLowerCase(),
)
) {
return JSON.stringify(value);
}
// Simple strings without special chars/meaning don't need quotes
return value;
} else if (
typeof value === "number" || typeof value === "boolean" || value === null
) {
return String(value);
}
// Default for unsupported scalar types (e.g., undefined)
return "null";
}
// Updated helper function to serialize various JS types to YAML string representations
// Added baseIndentation parameter for handling nested lists correctly (though we only use it for top-level lists here)
function serializeToYamlValue(
value: any,
baseIndentation: string = "",
): string {
if (Array.isArray(value)) {
if (value.length === 0) {
return "[]"; // Use flow style for empty arrays for simplicity
}
// Determine indentation for list items (base + 2 spaces)
const itemIndentation = baseIndentation + " ";
// Format each item recursively, preceded by '- ' marker
return "\n" +
value.map((item) =>
`${itemIndentation}- ${serializeToYamlValue(item, itemIndentation)}`
).join("\n");
// Note: serializeToYamlValue is used recursively here to handle nested arrays/objects if needed in future
// However, the current `applyMinimalSetKeyPatches` only handles top-level keys.
} else if (typeof value === "object" && value !== null) {
// Basic object serialization (not requested, but good to consider)
// This is highly simplified and doesn't handle nesting well without more context
const itemIndentation = baseIndentation + " ";
const entries = Object.entries(value);
if (entries.length === 0) return "{}"; // Flow style empty objects
return "\n" +
entries.map(([key, val]) =>
`${itemIndentation}${key}: ${
serializeToYamlValue(val, itemIndentation)
}`
).join("\n");
} else {
// Handle scalars using the dedicated function
return serializeToYamlScalar(value);
}
}
export function applyPatches(
yamlString: string,
patches: SetKeyPatch[],
): string {
let currentYaml = yamlString;
for (const patch of patches) {
if (patch.op !== "set-key") continue;
// Still simplifying: Only handle top-level keys
if (patch.path.includes(".") || patch.path.includes("[")) {
console.warn(
`Skipping patch for nested or invalid key path (not supported): ${patch.path}`,
);
continue;
}
const key = patch.path;
// Split the YAML into lines for easier processing
const lines = currentYaml.split("\n");
let keyLineIndex = -1;
let commentBlock = "";
let trailingComments = "";
let inlineComment = "";
// Find the key line and collect comments
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
if (trimmedLine.startsWith(key + ":")) {
keyLineIndex = i;
// Extract inline comment if present
const commentMatch = line.match(/#.*$/);
if (commentMatch) {
inlineComment = commentMatch[0];
}
// Look backwards for comments
for (let j = i - 1; j >= 0; j--) {
const prevLine = lines[j].trim();
if (prevLine.startsWith("#")) {
commentBlock = lines[j] + "\n" + commentBlock;
} else if (prevLine !== "") {
break;
}
}
// Look forwards for comments
for (let j = i + 1; j < lines.length; j++) {
const nextLine = lines[j].trim();
if (nextLine.startsWith("#")) {
trailingComments += lines[j] + "\n";
} else if (nextLine !== "") {
break;
}
}
break;
}
}
// Serialize the new value
const serializedNewValue = serializeToYamlValue(patch.value);
// Create the replacement line
let replacementLine: string;
if (serializedNewValue.startsWith("\n")) {
// For lists/objects, the key line ends with just ':'
replacementLine = `${key}:${inlineComment}${serializedNewValue}`;
} else {
// For scalars, format as key: value
replacementLine = `${key}: ${serializedNewValue}${
inlineComment ? " " + inlineComment : ""
}`;
}
if (keyLineIndex !== -1) {
// Replace the existing line while preserving comments
const beforeKey = lines.slice(
0,
keyLineIndex - commentBlock.split("\n").filter(Boolean).length,
);
const afterKey = lines.slice(
keyLineIndex + 1 + trailingComments.split("\n").filter(Boolean).length,
);
// Build the new content
const newContent = [
...beforeKey,
...commentBlock.split("\n").filter(Boolean),
replacementLine,
...trailingComments.split("\n").filter(Boolean),
...afterKey,
];
// Join lines and ensure proper newlines
currentYaml = newContent.join("\n").replace(/\n*$/, "\n") + "\n";
} else {
// Key not found: Add the new key-value pair to the end
const newLineBlock = replacementLine;
if (currentYaml.trim() === "") {
currentYaml = newLineBlock + "\n";
} else {
currentYaml = currentYaml.replace(/\n*$/, "\n") + newLineBlock + "\n";
}
}
}
// Ensure the result ends with a newline
return currentYaml.replace(/\n*$/, "\n");
}