718 lines
23 KiB
JavaScript
718 lines
23 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* A self-contained JavaScript utility for applying human-readable
|
||
* "pseudo-diff" patch files to a collection of text files.
|
||
*
|
||
|
||
|
||
|
||
|
||
|
||
*** Begin Patch
|
||
[YOUR_PATH]
|
||
*** End Patch
|
||
|
||
|
||
Where [YOUR_PATCH] is the actual content of your patch, specified in the following V4A diff format.
|
||
|
||
*** [ACTION] File: [path/to/file] -> ACTION can be one of Add, Update, or Delete.
|
||
For each snippet of code that needs to be changed, repeat the following:
|
||
[context_before] -> See below for further instructions on context.
|
||
- [old_code] -> Precede the old code with a minus sign.
|
||
+ [new_code] -> Precede the new, replacement code with a plus sign.
|
||
[context_after] -> See below for further instructions on context.
|
||
|
||
For instructions on [context_before] and [context_after]:
|
||
- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines.
|
||
- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
|
||
@@ class BaseClass
|
||
[3 lines of pre-context]
|
||
- [old_code]
|
||
+ [new_code]
|
||
[3 lines of post-context]
|
||
|
||
- If a code block is repeated so many times in a class or function such that even a single @@ statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance:
|
||
|
||
@@ class BaseClass
|
||
@@ def method():
|
||
[3 lines of pre-context]
|
||
- [old_code]
|
||
+ [new_code]
|
||
[3 lines of post-context]
|
||
|
||
Note, then, that we do not use line numbers in this diff format, as the context is enough to uniquely identify code. An example of a message that you might pass as "input" to this function, in order to apply a patch, is shown below.
|
||
|
||
*
|
||
*
|
||
*
|
||
*/
|
||
|
||
// --------------------------------------------------------------------------- //
|
||
// Domain objects
|
||
// --------------------------------------------------------------------------- //
|
||
class ActionType {
|
||
static ADD = "add";
|
||
static DELETE = "delete";
|
||
static UPDATE = "update";
|
||
}
|
||
|
||
class FileChange {
|
||
constructor(type, old_content = null, new_content = null, move_path = null) {
|
||
this.type = type;
|
||
this.old_content = old_content;
|
||
this.new_content = new_content;
|
||
this.move_path = move_path;
|
||
}
|
||
}
|
||
|
||
class Commit {
|
||
constructor() {
|
||
this.changes = {};
|
||
}
|
||
}
|
||
|
||
// --------------------------------------------------------------------------- //
|
||
// Exceptions
|
||
// --------------------------------------------------------------------------- //
|
||
class DiffError extends Error {
|
||
/** Any problem detected while parsing or applying a patch. */
|
||
constructor(message) {
|
||
super(message);
|
||
this.name = "DiffError";
|
||
}
|
||
}
|
||
|
||
// --------------------------------------------------------------------------- //
|
||
// Helper classes used while parsing patches
|
||
// --------------------------------------------------------------------------- //
|
||
class Chunk {
|
||
constructor(orig_index = -1, del_lines = [], ins_lines = []) {
|
||
this.orig_index = orig_index;
|
||
this.del_lines = del_lines;
|
||
this.ins_lines = ins_lines;
|
||
}
|
||
}
|
||
|
||
class PatchAction {
|
||
constructor(type, new_file = null, chunks = [], move_path = null) {
|
||
this.type = type;
|
||
this.new_file = new_file;
|
||
this.chunks = chunks;
|
||
this.move_path = move_path;
|
||
}
|
||
}
|
||
|
||
class Patch {
|
||
constructor() {
|
||
this.actions = {};
|
||
}
|
||
}
|
||
|
||
// --------------------------------------------------------------------------- //
|
||
// Patch text parser
|
||
// --------------------------------------------------------------------------- //
|
||
class Parser {
|
||
constructor(current_files, lines) {
|
||
this.current_files = current_files;
|
||
this.lines = lines;
|
||
this.index = 0;
|
||
this.patch = new Patch();
|
||
this.fuzz = 0;
|
||
}
|
||
|
||
// ------------- low-level helpers -------------------------------------- //
|
||
_cur_line() {
|
||
if (this.index >= this.lines.length) {
|
||
throw new DiffError("Unexpected end of input while parsing patch");
|
||
}
|
||
return this.lines[this.index];
|
||
}
|
||
|
||
static _norm(line) {
|
||
/** Strip CR so comparisons work for both LF and CRLF input. */
|
||
return line.replace(/\r$/, "");
|
||
}
|
||
|
||
// ------------- scanning convenience ----------------------------------- //
|
||
is_done(prefixes = null) {
|
||
if (this.index >= this.lines.length) {
|
||
return true;
|
||
}
|
||
if (
|
||
prefixes &&
|
||
prefixes.length > 0
|
||
) {
|
||
const currentLine = Parser._norm(this._cur_line());
|
||
for (const prefix of prefixes) {
|
||
if (currentLine.startsWith(prefix)) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
startswith(prefix) {
|
||
return Parser._norm(this._cur_line()).startsWith(prefix);
|
||
}
|
||
|
||
read_str(prefix) {
|
||
/**
|
||
* Consume the current line if it starts with *prefix* and return the text
|
||
* **after** the prefix. Raises if prefix is empty.
|
||
*/
|
||
if (prefix === "") {
|
||
throw new Error("read_str() requires a non-empty prefix");
|
||
}
|
||
if (Parser._norm(this._cur_line()).startsWith(prefix)) {
|
||
const text = this._cur_line().substring(prefix.length);
|
||
this.index += 1;
|
||
return text;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
read_line() {
|
||
/** Return the current raw line and advance. */
|
||
const line = this._cur_line();
|
||
this.index += 1;
|
||
return line;
|
||
}
|
||
|
||
// ------------- public entry point -------------------------------------- //
|
||
parse() {
|
||
while (!this.is_done(["*** End Patch"])) {
|
||
// ---------- UPDATE ---------- //
|
||
let path = this.read_str("*** Update File: ");
|
||
if (path) {
|
||
if (path in this.patch.actions) {
|
||
throw new DiffError(`Duplicate update for file: ${path}`);
|
||
}
|
||
const move_to = this.read_str("*** Move to: ");
|
||
if (!(path in this.current_files)) {
|
||
throw new DiffError(`Update File Error - missing file: ${path}`);
|
||
}
|
||
const text = this.current_files[path];
|
||
const action = this._parse_update_file(text);
|
||
action.move_path = move_to || null;
|
||
this.patch.actions[path] = action;
|
||
continue;
|
||
}
|
||
|
||
// ---------- DELETE ---------- //
|
||
path = this.read_str("*** Delete File: ");
|
||
if (path) {
|
||
if (path in this.patch.actions) {
|
||
throw new DiffError(`Duplicate delete for file: ${path}`);
|
||
}
|
||
if (!(path in this.current_files)) {
|
||
throw new DiffError(`Delete File Error - missing file: ${path}`);
|
||
}
|
||
this.patch.actions[path] = new PatchAction(ActionType.DELETE);
|
||
continue;
|
||
}
|
||
|
||
// ---------- ADD ---------- //
|
||
path = this.read_str("*** Add File: ");
|
||
if (path) {
|
||
if (path in this.patch.actions) {
|
||
throw new DiffError(`Duplicate add for file: ${path}`);
|
||
}
|
||
if (path in this.current_files) {
|
||
throw new DiffError(`Add File Error - file already exists: ${path}`);
|
||
}
|
||
this.patch.actions[path] = this._parse_add_file();
|
||
continue;
|
||
}
|
||
|
||
throw new DiffError(`Unknown line while parsing: ${this._cur_line()}`);
|
||
}
|
||
|
||
if (!this.startswith("*** End Patch")) {
|
||
throw new DiffError("Missing *** End Patch sentinel");
|
||
}
|
||
this.index += 1; // consume sentinel
|
||
}
|
||
|
||
// ------------- section parsers ---------------------------------------- //
|
||
_parse_update_file(text) {
|
||
const action = new PatchAction(ActionType.UPDATE);
|
||
const lines = text.split("\n");
|
||
let index = 0;
|
||
while (!this.is_done([
|
||
"*** End Patch",
|
||
"*** Update File:",
|
||
"*** Delete File:",
|
||
"*** Add File:",
|
||
"*** End of File",
|
||
])) {
|
||
const def_str = this.read_str("@@ ");
|
||
let section_str = "";
|
||
if (!def_str && Parser._norm(this._cur_line()) === "@@") {
|
||
section_str = this.read_line();
|
||
}
|
||
|
||
if (!(def_str || section_str || index === 0)) {
|
||
throw new DiffError(`Invalid line in update section:\n${this._cur_line()}`);
|
||
}
|
||
|
||
if (def_str && def_str.trim()) {
|
||
let found = false;
|
||
if (!lines.slice(0, index).includes(def_str)) {
|
||
for (let i = index; i < lines.length; i++) {
|
||
if (lines[i] === def_str) {
|
||
index = i + 1;
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!found && !lines.slice(0, index).map(s => s.trim()).includes(def_str.trim())) {
|
||
for (let i = index; i < lines.length; i++) {
|
||
if (lines[i].trim() === def_str.trim()) {
|
||
index = i + 1;
|
||
this.fuzz += 1;
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const [next_ctx, chunks, end_idx, eof] = peek_next_section(this.lines, this.index);
|
||
const [new_index, fuzz] = find_context(lines, next_ctx, index, eof);
|
||
if (new_index === -1) {
|
||
const ctx_txt = next_ctx.join("\n");
|
||
throw new DiffError(
|
||
`Invalid ${eof ? 'EOF ' : ''}context at ${index}:\n${ctx_txt}`
|
||
);
|
||
}
|
||
this.fuzz += fuzz;
|
||
for (const ch of chunks) {
|
||
ch.orig_index += new_index;
|
||
action.chunks.push(ch);
|
||
}
|
||
index = new_index + next_ctx.length;
|
||
this.index = end_idx;
|
||
}
|
||
return action;
|
||
}
|
||
|
||
_parse_add_file() {
|
||
const lines = [];
|
||
while (!this.is_done([
|
||
"*** End Patch",
|
||
"*** Update File:",
|
||
"*** Delete File:",
|
||
"*** Add File:"
|
||
])) {
|
||
const s = this.read_line();
|
||
if (!s.startsWith("+")) {
|
||
throw new DiffError(`Invalid Add File line (missing '+'): ${s}`);
|
||
}
|
||
lines.push(s.substring(1)); // strip leading '+'
|
||
}
|
||
return new PatchAction(ActionType.ADD, lines.join("\n"));
|
||
}
|
||
}
|
||
|
||
// --------------------------------------------------------------------------- //
|
||
// Helper functions
|
||
// --------------------------------------------------------------------------- //
|
||
function find_context_core(lines, context, start) {
|
||
if (!context || context.length === 0) {
|
||
return [start, 0];
|
||
}
|
||
|
||
// Exact match
|
||
for (let i = start; i <= lines.length - context.length; i++) {
|
||
let match = true;
|
||
for (let j = 0; j < context.length; j++) {
|
||
if (lines[i + j] !== context[j]) {
|
||
match = false;
|
||
break;
|
||
}
|
||
}
|
||
if (match) {
|
||
return [i, 0];
|
||
}
|
||
}
|
||
|
||
// Rstrip match
|
||
for (let i = start; i <= lines.length - context.length; i++) {
|
||
let match = true;
|
||
for (let j = 0; j < context.length; j++) {
|
||
if (lines[i + j].replace(/\s+$/, "") !== context[j].replace(/\s+$/, "")) {
|
||
match = false;
|
||
break;
|
||
}
|
||
}
|
||
if (match) {
|
||
return [i, 1];
|
||
}
|
||
}
|
||
|
||
// Trim match
|
||
for (let i = start; i <= lines.length - context.length; i++) {
|
||
let match = true;
|
||
for (let j = 0; j < context.length; j++) {
|
||
if (lines[i + j].trim() !== context[j].trim()) {
|
||
match = false;
|
||
break;
|
||
}
|
||
}
|
||
if (match) {
|
||
return [i, 100];
|
||
}
|
||
}
|
||
|
||
return [-1, 0];
|
||
}
|
||
|
||
function find_context(lines, context, start, eof) {
|
||
if (eof) {
|
||
let [new_index, fuzz] = find_context_core(lines, context, Math.max(0, lines.length - context.length));
|
||
if (new_index !== -1) {
|
||
return [new_index, fuzz];
|
||
}
|
||
[new_index, fuzz] = find_context_core(lines, context, start);
|
||
return [new_index, fuzz + 10000];
|
||
}
|
||
return find_context_core(lines, context, start);
|
||
}
|
||
|
||
function peek_next_section(lines, index) {
|
||
const old = [];
|
||
let del_lines = [];
|
||
let ins_lines = [];
|
||
const chunks = [];
|
||
let mode = "keep";
|
||
const orig_index = index;
|
||
|
||
while (index < lines.length) {
|
||
let s = lines[index];
|
||
if (s.startsWith("@@") ||
|
||
s === "*** End Patch" ||
|
||
s.startsWith("*** Update File:") ||
|
||
s.startsWith("*** Delete File:") ||
|
||
s.startsWith("*** Add File:") ||
|
||
s === "*** End of File") {
|
||
break;
|
||
}
|
||
if (s === "***") {
|
||
break;
|
||
}
|
||
if (s.startsWith("***")) {
|
||
throw new DiffError(`Invalid Line: ${s}`);
|
||
}
|
||
index += 1;
|
||
|
||
const last_mode = mode;
|
||
if (s === "") {
|
||
s = " ";
|
||
}
|
||
if (s[0] === "+") {
|
||
mode = "add";
|
||
} else if (s[0] === "-") {
|
||
mode = "delete";
|
||
} else if (s[0] === " ") {
|
||
mode = "keep";
|
||
} else {
|
||
throw new DiffError(`Invalid Line: ${s}`);
|
||
}
|
||
s = s.substring(1);
|
||
|
||
if (mode === "keep" && last_mode !== mode) {
|
||
if (ins_lines.length > 0 || del_lines.length > 0) {
|
||
chunks.push(
|
||
new Chunk(
|
||
old.length - del_lines.length,
|
||
[...del_lines],
|
||
[...ins_lines]
|
||
)
|
||
);
|
||
}
|
||
del_lines = [];
|
||
ins_lines = [];
|
||
}
|
||
|
||
if (mode === "delete") {
|
||
del_lines.push(s);
|
||
old.push(s);
|
||
} else if (mode === "add") {
|
||
ins_lines.push(s);
|
||
} else if (mode === "keep") {
|
||
old.push(s);
|
||
}
|
||
}
|
||
|
||
if (ins_lines.length > 0 || del_lines.length > 0) {
|
||
chunks.push(
|
||
new Chunk(
|
||
old.length - del_lines.length,
|
||
[...del_lines],
|
||
[...ins_lines]
|
||
)
|
||
);
|
||
}
|
||
|
||
if (index < lines.length && lines[index] === "*** End of File") {
|
||
index += 1;
|
||
return [old, chunks, index, true];
|
||
}
|
||
|
||
if (index === orig_index) {
|
||
throw new DiffError("Nothing in this section");
|
||
}
|
||
return [old, chunks, index, false];
|
||
}
|
||
|
||
// --------------------------------------------------------------------------- //
|
||
// Patch → Commit and Commit application
|
||
// --------------------------------------------------------------------------- //
|
||
function _get_updated_file(text, action, path) {
|
||
if (action.type !== ActionType.UPDATE) {
|
||
throw new DiffError("_get_updated_file called with non-update action");
|
||
}
|
||
const orig_lines = text.split("\n");
|
||
const dest_lines = [];
|
||
let orig_index = 0;
|
||
|
||
for (const chunk of action.chunks) {
|
||
if (chunk.orig_index > orig_lines.length) {
|
||
throw new DiffError(
|
||
`${path}: chunk.orig_index ${chunk.orig_index} exceeds file length`
|
||
);
|
||
}
|
||
if (orig_index > chunk.orig_index) {
|
||
throw new DiffError(
|
||
`${path}: overlapping chunks at ${orig_index} > ${chunk.orig_index}`
|
||
);
|
||
}
|
||
|
||
// Add lines before the chunk
|
||
for (let i = orig_index; i < chunk.orig_index; i++) {
|
||
dest_lines.push(orig_lines[i]);
|
||
}
|
||
orig_index = chunk.orig_index;
|
||
|
||
// Add inserted lines
|
||
dest_lines.push(...chunk.ins_lines);
|
||
|
||
// Skip deleted lines
|
||
orig_index += chunk.del_lines.length;
|
||
}
|
||
|
||
// Add remaining lines
|
||
for (let i = orig_index; i < orig_lines.length; i++) {
|
||
dest_lines.push(orig_lines[i]);
|
||
}
|
||
|
||
return dest_lines.join("\n");
|
||
}
|
||
|
||
function patch_to_commit(patch, orig) {
|
||
const commit = new Commit();
|
||
for (const [path, action] of Object.entries(patch.actions)) {
|
||
if (action.type === ActionType.DELETE) {
|
||
commit.changes[path] = new FileChange(
|
||
ActionType.DELETE,
|
||
orig[path],
|
||
null,
|
||
null
|
||
);
|
||
} else if (action.type === ActionType.ADD) {
|
||
if (action.new_file === null) {
|
||
throw new DiffError("ADD action without file content");
|
||
}
|
||
commit.changes[path] = new FileChange(
|
||
ActionType.ADD,
|
||
null,
|
||
action.new_file,
|
||
null
|
||
);
|
||
} else if (action.type === ActionType.UPDATE) {
|
||
const new_content = _get_updated_file(orig[path], action, path);
|
||
commit.changes[path] = new FileChange(
|
||
ActionType.UPDATE,
|
||
orig[path],
|
||
new_content,
|
||
action.move_path
|
||
);
|
||
}
|
||
}
|
||
return commit;
|
||
}
|
||
|
||
// --------------------------------------------------------------------------- //
|
||
// User-facing helpers
|
||
// --------------------------------------------------------------------------- //
|
||
function text_to_patch(text, orig) {
|
||
// Handle different line ending scenarios
|
||
let lines = text.split(/\r?\n/);
|
||
|
||
// Remove trailing empty line if it exists (from trailing newline)
|
||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
||
lines = lines.slice(0, -1);
|
||
}
|
||
|
||
// Debug logging
|
||
console.log("Lines count:", lines.length);
|
||
console.log("First line:", JSON.stringify(lines[0]));
|
||
if (lines.length > 0) {
|
||
console.log("Last line:", JSON.stringify(lines[lines.length - 1]));
|
||
console.log("First line normalized:", JSON.stringify(Parser._norm(lines[0])));
|
||
console.log("Last line normalized:", JSON.stringify(Parser._norm(lines[lines.length - 1])));
|
||
}
|
||
|
||
if (
|
||
lines.length < 2 ||
|
||
!Parser._norm(lines[0]).startsWith("*** Begin Patch") ||
|
||
(lines.length > 0 && Parser._norm(lines[lines.length - 1]) !== "*** End Patch")
|
||
) {
|
||
throw new DiffError("Invalid patch text - missing sentinels");
|
||
}
|
||
|
||
const parser = new Parser(orig, lines);
|
||
parser.index = 1;
|
||
parser.parse();
|
||
return [parser.patch, parser.fuzz];
|
||
}
|
||
|
||
function identify_files_needed(text) {
|
||
// Handle line splitting consistently
|
||
let lines = text.split(/\r?\n/);
|
||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
||
lines = lines.slice(0, -1);
|
||
}
|
||
|
||
const updateFiles = lines
|
||
.filter(line => line.startsWith("*** Update File: "))
|
||
.map(line => line.substring("*** Update File: ".length));
|
||
|
||
const deleteFiles = lines
|
||
.filter(line => line.startsWith("*** Delete File: "))
|
||
.map(line => line.substring("*** Delete File: ".length));
|
||
|
||
return [...updateFiles, ...deleteFiles];
|
||
}
|
||
|
||
function identify_files_added(text) {
|
||
// Handle line splitting consistently
|
||
let lines = text.split(/\r?\n/);
|
||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
||
lines = lines.slice(0, -1);
|
||
}
|
||
|
||
return lines
|
||
.filter(line => line.startsWith("*** Add File: "))
|
||
.map(line => line.substring("*** Add File: ".length));
|
||
}
|
||
|
||
// --------------------------------------------------------------------------- //
|
||
// File-system helpers
|
||
// --------------------------------------------------------------------------- //
|
||
function load_files(paths, open_fn) {
|
||
const result = {};
|
||
for (const path of paths) {
|
||
result[path] = open_fn(path);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function apply_commit(commit, write_fn, remove_fn) {
|
||
for (const [path, change] of Object.entries(commit.changes)) {
|
||
if (change.type === ActionType.DELETE) {
|
||
remove_fn(path);
|
||
} else if (change.type === ActionType.ADD) {
|
||
if (change.new_content === null) {
|
||
throw new DiffError(`ADD change for ${path} has no content`);
|
||
}
|
||
write_fn(path, change.new_content);
|
||
} else if (change.type === ActionType.UPDATE) {
|
||
if (change.new_content === null) {
|
||
throw new DiffError(`UPDATE change for ${path} has no new content`);
|
||
}
|
||
const target = change.move_path || path;
|
||
write_fn(target, change.new_content);
|
||
if (change.move_path) {
|
||
remove_fn(path);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function process_patch(text, open_fn, write_fn, remove_fn) {
|
||
if (!text.startsWith("*** Begin Patch")) {
|
||
throw new DiffError("Patch text must start with *** Begin Patch");
|
||
}
|
||
const paths = identify_files_needed(text);
|
||
const orig = load_files(paths, open_fn);
|
||
const [patch, _fuzz] = text_to_patch(text, orig);
|
||
const commit = patch_to_commit(patch, orig);
|
||
apply_commit(commit, write_fn, remove_fn);
|
||
return "Done!";
|
||
}
|
||
|
||
// --------------------------------------------------------------------------- //
|
||
// Default FS helpers
|
||
// --------------------------------------------------------------------------- //
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
|
||
function open_file(filepath) {
|
||
return fs.readFileSync(filepath, 'utf8');
|
||
}
|
||
|
||
function write_file(filepath, content) {
|
||
const dir = path.dirname(filepath);
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
fs.writeFileSync(filepath, content, 'utf8');
|
||
}
|
||
|
||
function remove_file(filepath) {
|
||
if (fs.existsSync(filepath)) {
|
||
fs.unlinkSync(filepath);
|
||
}
|
||
}
|
||
|
||
// Export for the new interface
|
||
export default {
|
||
type: "function",
|
||
name: "patch_files",
|
||
description: "Apply a unified diff patch to files within a chroot directory, with option to reverse the patch",
|
||
parameters: {
|
||
type: "object",
|
||
properties: {
|
||
patch: {
|
||
type: "string",
|
||
description: "The unidiff patch string to apply.",
|
||
}
|
||
},
|
||
required: ["patch"],
|
||
additionalProperties: false,
|
||
},
|
||
strict: true,
|
||
};
|
||
|
||
export async function run(args) {
|
||
console.log('patch_files:', args);
|
||
|
||
try {
|
||
const result = process_patch(
|
||
args.patch,
|
||
open_file,
|
||
write_file,
|
||
remove_file
|
||
);
|
||
return result;
|
||
} catch (error) {
|
||
if (error instanceof DiffError) {
|
||
throw new Error(`Patch error: ${error.message}`);
|
||
}
|
||
throw error;
|
||
}
|
||
} |