- .archify-registry opens in a new read-only custom editor (archify.registry, default): the file's elements as cards; click one for a dialog with its description, the defining line (opens the YAML) and the diagrams that use it (open on click); Show Source and Open Model work from the view - the same dialog opens from the registry cards on the model page - Archify's node index is titled Elements; a Connectors panel built from the SVG edges lists from → to with the label, highlights on hover and keeps the highlight on click; side by side in the bottom rail - integration test for the registry view; docs and changelog
403 lines
22 KiB
TypeScript
403 lines
22 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import path from 'node:path';
|
|
import * as vscode from 'vscode';
|
|
|
|
const EXTENSION_ID = 'root-at-skic.archify-vscode-ext';
|
|
|
|
function workspaceFile(name: string): vscode.Uri {
|
|
const folder = vscode.workspace.workspaceFolders?.[0];
|
|
assert.ok(folder, 'a workspace folder is open');
|
|
return vscode.Uri.joinPath(folder.uri, name);
|
|
}
|
|
|
|
async function waitFor<T>(what: string, probe: () => T | undefined | false, timeoutMs = 20_000): Promise<T> {
|
|
const start = Date.now();
|
|
for (;;) {
|
|
const value = probe();
|
|
if (value) return value;
|
|
if (Date.now() - start > timeoutMs) throw new Error(`Timed out waiting for ${what}`);
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
}
|
|
}
|
|
|
|
function webviewTabs(viewTypeSuffix: string): vscode.Tab[] {
|
|
return vscode.window.tabGroups.all
|
|
.flatMap((group) => group.tabs)
|
|
.filter((tab) =>
|
|
(tab.input instanceof vscode.TabInputWebview && tab.input.viewType.endsWith(viewTypeSuffix)) ||
|
|
(tab.input instanceof vscode.TabInputCustom && tab.input.viewType === viewTypeSuffix));
|
|
}
|
|
|
|
async function check(name: string, body: () => Promise<void>): Promise<void> {
|
|
try {
|
|
await body();
|
|
console.log(` ✔ ${name}`);
|
|
} catch (error) {
|
|
console.log(` ✖ ${name}\n ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function run(): Promise<void> {
|
|
console.log('Archify extension integration suite');
|
|
|
|
await check('activates on a diagram source and sets the context key', async () => {
|
|
const doc = await vscode.workspace.openTextDocument(workspaceFile('production-deployment.architecture.json'));
|
|
await vscode.window.showTextDocument(doc);
|
|
const extension = vscode.extensions.getExtension(EXTENSION_ID);
|
|
assert.ok(extension, 'extension is installed');
|
|
await waitFor('activation', () => extension.isActive);
|
|
});
|
|
|
|
await check('clicking a .archify file opens the diagram, not the text', async () => {
|
|
const uri = workspaceFile('web-platform.archify');
|
|
await vscode.commands.executeCommand('vscode.open', uri);
|
|
const tab = await waitFor('diagram editor tab', () =>
|
|
vscode.window.tabGroups.all
|
|
.flatMap((group) => group.tabs)
|
|
.find((t) => t.input instanceof vscode.TabInputCustom && t.input.viewType === 'archify.diagram' && t.input.uri.toString() === uri.toString()),
|
|
);
|
|
assert.ok(tab.isActive, 'the diagram tab is active');
|
|
assert.equal(vscode.window.activeTextEditor?.document.uri.toString() === uri.toString(), false, 'no text editor was opened for it');
|
|
});
|
|
|
|
await check('.archify files are YAML and Show Source opens the text beside the diagram', async () => {
|
|
const uri = workspaceFile('web-platform.archify');
|
|
await vscode.commands.executeCommand('archify.showSource');
|
|
const editor = await waitFor('source text editor', () =>
|
|
vscode.window.visibleTextEditors.find((e) => e.document.uri.toString() === uri.toString()),
|
|
);
|
|
assert.equal(editor.document.languageId, 'yaml');
|
|
assert.match(editor.document.getText(), /^schema_version: 1/m);
|
|
});
|
|
|
|
await check('.archify validation problems are reported against the YAML', async () => {
|
|
const good = await vscode.workspace.openTextDocument(workspaceFile('cache-miss.archify'));
|
|
const broken = good.getText().replace(/^participants:/m, 'actors:');
|
|
const target = workspaceFile('broken.archify');
|
|
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode(broken));
|
|
const doc = await vscode.workspace.openTextDocument(target);
|
|
await vscode.window.showTextDocument(doc);
|
|
const diagnostics = await waitFor('archify diagnostics on broken.archify', () => {
|
|
const found = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify');
|
|
return found.length > 0 && found;
|
|
});
|
|
assert.ok(diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error));
|
|
});
|
|
|
|
await check('.archify YAML syntax errors are reported at their line', async () => {
|
|
const target = workspaceFile('syntax.archify');
|
|
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode('schema_version: 1\ndiagram_type: architecture\nmeta: [unclosed\n'));
|
|
const doc = await vscode.workspace.openTextDocument(target);
|
|
await vscode.window.showTextDocument(doc);
|
|
const diagnostics = await waitFor('yaml syntax diagnostics', () => {
|
|
const found = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify' && d.message.startsWith('YAML:'));
|
|
return found.length > 0 && found;
|
|
});
|
|
assert.ok(diagnostics[0].range.start.line >= 2, `reported on line ${diagnostics[0].range.start.line + 1}`);
|
|
});
|
|
|
|
await check('Archify | New Diagram creates a .archify file from a template and opens the diagram', async () => {
|
|
// Menus show the bare title, so it must name Archify by itself.
|
|
const commands = vscode.extensions.getExtension(EXTENSION_ID)?.packageJSON.contributes.commands as { command: string; title: string; category?: string }[];
|
|
const newDiagram = commands.find((item) => item.command === 'archify.newDiagram');
|
|
assert.equal(newDiagram?.title, 'Archify | New Diagram…');
|
|
assert.equal(newDiagram?.category, undefined);
|
|
const folder = workspaceFile('.');
|
|
// Stub the template pick and the name prompt; check the name validation on the way.
|
|
const window = vscode.window as unknown as Record<string, unknown>;
|
|
const originalPick = window.showQuickPick;
|
|
const originalInput = window.showInputBox;
|
|
let existingNameMessage: unknown;
|
|
window.showQuickPick = async (items: readonly { label: string }[]) => (await items).find((item) => item.label === 'Everyday Plan');
|
|
window.showInputBox = async (options: vscode.InputBoxOptions) => {
|
|
existingNameMessage = await options.validateInput?.('web-platform');
|
|
return 'summer-plan';
|
|
};
|
|
try {
|
|
await vscode.commands.executeCommand('archify.newDiagram', folder);
|
|
} finally {
|
|
window.showQuickPick = originalPick;
|
|
window.showInputBox = originalInput;
|
|
}
|
|
assert.match(String(existingNameMessage), /already exists/);
|
|
const uri = vscode.Uri.joinPath(folder, 'summer-plan.archify');
|
|
const text = new TextDecoder().decode(await vscode.workspace.fs.readFile(uri));
|
|
assert.match(text, /^diagram_type: workflow$/m);
|
|
assert.match(text, /^ {2}title: Summer plan$/m);
|
|
assert.match(text, /^ {2}output: summer-plan\.html$/m);
|
|
await waitFor('diagram editor for the new file', () =>
|
|
webviewTabs('archify.diagram').some((tab) => tab.input instanceof vscode.TabInputCustom && tab.input.uri.toString() === uri.toString()),
|
|
);
|
|
});
|
|
|
|
await check('suggests brand IDs after brand: in .archify files', async () => {
|
|
const target = workspaceFile('brands.archify');
|
|
const text = [
|
|
'schema_version: 1',
|
|
'diagram_type: architecture',
|
|
'meta:',
|
|
' title: Brands',
|
|
'components:',
|
|
' - id: db',
|
|
' type: database',
|
|
' label: DB',
|
|
' brand: ',
|
|
' - id: bus',
|
|
' type: messagebus',
|
|
' label: Bus',
|
|
' brand: kafka',
|
|
'',
|
|
].join('\n');
|
|
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode(text));
|
|
const doc = await vscode.workspace.openTextDocument(target);
|
|
const labels = async (line: number) => {
|
|
const list = await vscode.commands.executeCommand<vscode.CompletionList>(
|
|
'vscode.executeCompletionItemProvider',
|
|
doc.uri,
|
|
new vscode.Position(line, doc.lineAt(line).text.length),
|
|
);
|
|
return list.items.map((item) => (typeof item.label === 'string' ? item.label : item.label.label));
|
|
};
|
|
const empty = await labels(8);
|
|
assert.ok(empty.includes('postgresql') && empty.includes('redis'), 'brand IDs offered after "brand: "');
|
|
const kafka = await labels(12);
|
|
assert.ok(kafka.includes('apache-kafka'), 'the alias "kafka" finds apache-kafka');
|
|
assert.equal((await labels(7)).includes('postgresql'), false, 'nothing offered for other fields');
|
|
});
|
|
|
|
await check('an unknown brand is reported on its own line', async () => {
|
|
const target = workspaceFile('unknown-brand.archify');
|
|
const text = new TextDecoder().decode(await vscode.workspace.fs.readFile(workspaceFile('web-platform.archify')))
|
|
.replace(/^( {2}- id: [^\n]+\n)/m, '$1 brand: not-a-brand\n');
|
|
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode(text));
|
|
const doc = await vscode.workspace.openTextDocument(target);
|
|
await vscode.window.showTextDocument(doc);
|
|
const diagnostic = await waitFor('brand/unknown diagnostic', () =>
|
|
vscode.languages.getDiagnostics(doc.uri).find((d) => d.source === 'archify' && d.code === 'brand/unknown'),
|
|
);
|
|
assert.match(doc.lineAt(diagnostic.range.start.line).text, /brand: not-a-brand/);
|
|
});
|
|
|
|
await check('opens a live preview beside the source', async () => {
|
|
await vscode.commands.executeCommand('archify.showPreviewToSide', workspaceFile('production-deployment.architecture.json'));
|
|
const tabs = await waitFor('preview tab', () => {
|
|
const found = webviewTabs('archify.preview').filter((tab) => /production-deployment/.test(tab.label));
|
|
return found.length > 0 && found;
|
|
});
|
|
assert.equal(tabs.length, 1);
|
|
assert.match(tabs[0].label, /Preview production-deployment\.architecture\.json/);
|
|
});
|
|
|
|
await check('reports validation problems in the Problems panel with JSON ranges', async () => {
|
|
const doc = await vscode.workspace.openTextDocument({
|
|
language: 'json',
|
|
content: JSON.stringify({ schema_version: 1, diagram_type: 'architecture', meta: { title: 'x' }, components: [{ id: 'a' }] }, null, 2),
|
|
});
|
|
await vscode.window.showTextDocument(doc);
|
|
const diagnostics = await waitFor('archify diagnostics', () => {
|
|
const found = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify');
|
|
return found.length > 0 && found;
|
|
});
|
|
const component = diagnostics.find((d) => /components\/0/.test(d.message));
|
|
assert.ok(component, 'a diagnostic for /components/0');
|
|
assert.equal(doc.getText(component.range), '{', 'range points at the component object');
|
|
assert.equal(component.severity, vscode.DiagnosticSeverity.Error);
|
|
});
|
|
|
|
await check('clears problems once the document is fixed', async () => {
|
|
const good = await vscode.workspace.openTextDocument(workspaceFile('event-stream.dataflow.json'));
|
|
const doc = await vscode.workspace.openTextDocument({ language: 'json', content: good.getText() });
|
|
await vscode.window.showTextDocument(doc);
|
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
const errors = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify' && d.severity === vscode.DiagnosticSeverity.Error);
|
|
assert.deepEqual(errors.map((d) => d.message), []);
|
|
});
|
|
|
|
await check('renders to an HTML file and opens it in the Archify viewer', async () => {
|
|
const source = workspaceFile('cache-miss-request.sequence.json');
|
|
const output = vscode.Uri.file(path.join(path.dirname(source.fsPath), 'cache-miss-request.sequence.html'));
|
|
// Stub the save dialog and the follow-up notification.
|
|
const window = vscode.window as unknown as Record<string, unknown>;
|
|
const originalSave = window.showSaveDialog;
|
|
const originalInfo = window.showInformationMessage;
|
|
window.showSaveDialog = async () => output;
|
|
window.showInformationMessage = async () => 'Open in Viewer';
|
|
try {
|
|
await vscode.commands.executeCommand('archify.renderToHtml', source);
|
|
} finally {
|
|
window.showSaveDialog = originalSave;
|
|
window.showInformationMessage = originalInfo;
|
|
}
|
|
const html = new TextDecoder().decode(await vscode.workspace.fs.readFile(output));
|
|
assert.match(html, /<meta name="generator" content="archify/);
|
|
await waitFor('custom editor tab', () => webviewTabs('archify.htmlViewer').length > 0);
|
|
});
|
|
|
|
await check('source-backed diagram previews with the evidence fallback', async () => {
|
|
const uri = workspaceFile('mco-runtime.architecture.json');
|
|
await vscode.commands.executeCommand('archify.showPreview', uri);
|
|
await waitFor('mco preview tab', () => webviewTabs('archify.preview').some((tab) => /mco-runtime/.test(tab.label)));
|
|
const doc = await vscode.workspace.openTextDocument(uri);
|
|
const diagnostics = await waitFor('evidence diagnostics', () => {
|
|
const found = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify');
|
|
return found.length > 0 && found;
|
|
});
|
|
assert.ok(
|
|
diagnostics.every((d) => d.severity !== vscode.DiagnosticSeverity.Error),
|
|
'unverifiable evidence is a warning in fallback mode',
|
|
);
|
|
});
|
|
|
|
// --- models: a folder with main.archify, registries and diagrams that use `ref`
|
|
|
|
await check('a diagram inside a model resolves registry refs and validates clean', async () => {
|
|
const uri = workspaceFile('model/0200-containers/online-shop.archify');
|
|
const doc = await vscode.workspace.openTextDocument(uri);
|
|
await vscode.window.showTextDocument(doc);
|
|
// Validation runs through the Archify CLI; give it time, then expect no errors at all.
|
|
await new Promise((resolve) => setTimeout(resolve, 6000));
|
|
const errors = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify' && d.severity === vscode.DiagnosticSeverity.Error);
|
|
assert.deepEqual(errors.map((d) => d.message), []);
|
|
});
|
|
|
|
await check('an unknown ref is reported at its line', async () => {
|
|
const target = workspaceFile('model/0300-components/ghost.archify');
|
|
await vscode.workspace.fs.writeFile(
|
|
target,
|
|
new TextEncoder().encode('schema_version: 1\ndiagram_type: architecture\nmeta: { title: Ghost, output: ghost.html }\ncomponents:\n - ref: ghost\n pos: [40, 40]\n size: [130, 60]\n'),
|
|
);
|
|
const doc = await vscode.workspace.openTextDocument(target);
|
|
await vscode.window.showTextDocument(doc);
|
|
const diagnostic = await waitFor('unknown ref diagnostic', () =>
|
|
vscode.languages.getDiagnostics(doc.uri).find((d) => d.source === 'archify' && /Unknown element "ghost"/.test(d.message)),
|
|
);
|
|
assert.equal(diagnostic.range.start.line, 4, 'reported on the ref line');
|
|
});
|
|
|
|
await check('hover and completion on ref come from the registry', async () => {
|
|
const uri = workspaceFile('model/0200-containers/online-shop.archify');
|
|
const doc = await vscode.workspace.openTextDocument(uri);
|
|
const line = doc.getText().split('\n').findIndex((l) => /ref: api$/.test(l));
|
|
assert.ok(line >= 0, 'the containers diagram refers to api');
|
|
const hovers = await vscode.commands.executeCommand<vscode.Hover[]>('vscode.executeHoverProvider', uri, new vscode.Position(line, 10));
|
|
const text = hovers.flatMap((h) => h.contents).map((c) => (typeof c === 'string' ? c : c.value)).join('\n');
|
|
assert.match(text, /Owns authentication/);
|
|
const defs = await vscode.commands.executeCommand<vscode.Location[]>('vscode.executeDefinitionProvider', uri, new vscode.Position(line, 10));
|
|
assert.match(defs[0].uri.path, /registry\/containers\.archify-registry$/);
|
|
const completions = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', uri, new vscode.Position(line, 9));
|
|
const labels = completions.items.map((i) => (typeof i.label === 'string' ? i.label : i.label.label));
|
|
assert.ok(labels.includes('api') && labels.includes('queue'), `completion lists registry elements: ${labels.join(', ')}`);
|
|
});
|
|
|
|
await check('a registry file reports duplicate ids and parse problems', async () => {
|
|
const target = workspaceFile('model/registry/extra.archify-registry');
|
|
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode('schema_version: 1\nelements:\n api:\n type: backend\n label: API again\n'));
|
|
const doc = await vscode.workspace.openTextDocument(target);
|
|
await vscode.window.showTextDocument(doc);
|
|
const diagnostic = await waitFor('duplicate id diagnostic', () =>
|
|
vscode.languages.getDiagnostics(doc.uri).find((d) => d.source === 'archify' && /defined twice/.test(d.message)),
|
|
);
|
|
assert.equal(diagnostic.range.start.line, 2);
|
|
await vscode.workspace.fs.delete(target);
|
|
});
|
|
|
|
await check('main.archify opens as the model page and Open Model finds it from a diagram', async () => {
|
|
const main = workspaceFile('model/main.archify');
|
|
await vscode.commands.executeCommand('vscode.open', main);
|
|
await waitFor('model page tab', () =>
|
|
vscode.window.tabGroups.all.flatMap((g) => g.tabs).some((t) => t.input instanceof vscode.TabInputCustom && t.input.viewType === 'archify.diagram' && t.input.uri.toString() === main.toString()),
|
|
);
|
|
const diagram = workspaceFile('model/0100-context/online-shop.archify');
|
|
await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(diagram));
|
|
await vscode.commands.executeCommand('archify.openModel', diagram);
|
|
const tab = await waitFor('model page active', () =>
|
|
vscode.window.tabGroups.all.flatMap((g) => g.tabs).find((t) => t.isActive && t.input instanceof vscode.TabInputCustom && t.input.uri.toString() === main.toString()),
|
|
);
|
|
assert.ok(tab);
|
|
});
|
|
|
|
await check('a .archify-registry file opens as the registry view and Show Source opens the YAML', async () => {
|
|
const registry = workspaceFile('model/registry/people.archify-registry');
|
|
await vscode.commands.executeCommand('vscode.open', registry);
|
|
const tab = await waitFor('registry view tab', () =>
|
|
vscode.window.tabGroups.all.flatMap((g) => g.tabs).find((t) => t.isActive && t.input instanceof vscode.TabInputCustom && t.input.viewType === 'archify.registry' && t.input.uri.toString() === registry.toString()),
|
|
);
|
|
assert.ok(tab);
|
|
await vscode.commands.executeCommand('archify.showSource');
|
|
const editor = await waitFor('registry text editor', () => vscode.window.visibleTextEditors.find((e) => e.document.uri.toString() === registry.toString()));
|
|
assert.equal(editor.document.languageId, 'yaml');
|
|
});
|
|
|
|
await check('Archify | New Diagram… → Model creates main.archify from the folder name', async () => {
|
|
const folder = workspaceFile('new-shop');
|
|
await vscode.workspace.fs.createDirectory(folder);
|
|
const { window } = vscode;
|
|
const originalPick = window.showQuickPick;
|
|
(window as { showQuickPick: unknown }).showQuickPick = async (items: Thenable<{ label: string }[]> | { label: string }[]) => (await items).find((i) => /^Model/.test(i.label));
|
|
try {
|
|
await vscode.commands.executeCommand('archify.newDiagram', folder);
|
|
} finally {
|
|
window.showQuickPick = originalPick;
|
|
}
|
|
const main = vscode.Uri.joinPath(folder, 'main.archify');
|
|
let content = '';
|
|
for (let i = 0; i < 50 && !content; i++) {
|
|
try {
|
|
content = new TextDecoder().decode(await vscode.workspace.fs.readFile(main));
|
|
} catch {
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
}
|
|
}
|
|
assert.match(content, /id: new-shop/);
|
|
assert.match(content, /name: New shop/);
|
|
});
|
|
|
|
await check('Archify | New Model… creates a model folder from an example and opens its page', async () => {
|
|
const commands = vscode.extensions.getExtension(EXTENSION_ID)?.packageJSON.contributes.commands as { command: string; title: string }[];
|
|
assert.equal(commands.find((item) => item.command === 'archify.newModel')?.title, 'Archify | New Model…');
|
|
const parent = workspaceFile('.');
|
|
const window = vscode.window as unknown as Record<string, unknown>;
|
|
const originalPick = window.showQuickPick;
|
|
const originalInput = window.showInputBox;
|
|
let labels: string[] = [];
|
|
let existingNameMessage: unknown;
|
|
window.showQuickPick = async (items: readonly { label: string }[]) => {
|
|
labels = (await items).map((item) => item.label);
|
|
return (await items).find((item) => item.label === 'C4 - E-Shop');
|
|
};
|
|
window.showInputBox = async (options: vscode.InputBoxOptions) => {
|
|
existingNameMessage = await options.validateInput?.('new-shop');
|
|
return 'shop-two';
|
|
};
|
|
try {
|
|
await vscode.commands.executeCommand('archify.newModel', parent);
|
|
} finally {
|
|
window.showQuickPick = originalPick;
|
|
window.showInputBox = originalInput;
|
|
}
|
|
assert.deepEqual(labels, ['C4 - Standard Web App', 'C4 - E-Shop', 'C4 - Event-Based Integration']);
|
|
assert.match(String(existingNameMessage), /already exists/);
|
|
const folder = vscode.Uri.joinPath(parent, 'shop-two');
|
|
const main = vscode.Uri.joinPath(folder, 'main.archify');
|
|
const content = new TextDecoder().decode(await vscode.workspace.fs.readFile(main));
|
|
assert.match(content, /id: shop-two/);
|
|
assert.match(content, /name: Shop two/);
|
|
assert.match(content, /title: Shop two — overview/);
|
|
const names = (await vscode.workspace.fs.readDirectory(folder)).map(([name]) => name).sort();
|
|
assert.deepEqual(names, ['0100-context', '0200-containers', '0300-components', 'main.archify', 'registry']);
|
|
const registry = (await vscode.workspace.fs.readDirectory(vscode.Uri.joinPath(folder, 'registry'))).map(([name]) => name).sort();
|
|
assert.deepEqual(registry, ['containers.archify-registry', 'people.archify-registry', 'systems.archify-registry']);
|
|
const tab = await waitFor('model page tab', () =>
|
|
vscode.window.tabGroups.all.flatMap((g) => g.tabs).find((t) => t.isActive && t.input instanceof vscode.TabInputCustom && t.input.uri.toString() === main.toString()),
|
|
);
|
|
assert.ok(tab);
|
|
// The new model is indexed: its diagrams resolve against the copied registry.
|
|
const containers = await vscode.workspace.openTextDocument(vscode.Uri.joinPath(folder, '0200-containers', 'online-shop.archify'));
|
|
await vscode.window.showTextDocument(containers);
|
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
const errors = vscode.languages.getDiagnostics(containers.uri).filter((d) => d.severity === vscode.DiagnosticSeverity.Error);
|
|
assert.deepEqual(errors.map((d) => d.message), []);
|
|
});
|
|
}
|