New oracle DocuRun on Java's JDocuElement. Italic is kept (the model knew only bold), an item keeps its file's size and font size (Swift's 32 x 24 and 8-72 changed Java's files), an item without a colour is Java's light grey. Java's new items, colour lists, font sizes and styles, the drag's smallest sizes and Java's stacking; document commands for the editor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
209 lines
11 KiB
TypeScript
209 lines
11 KiB
TypeScript
// The documentation items against Java's documentation mode
|
||
// (documentation.java.json; tools/java-oracle's DocuRun on JDocuElement):
|
||
// a new text and rectangle, the colours and font sizes Java's menus offer,
|
||
// the font and colour chosen there, the smallest sizes a drag leaves, the
|
||
// stacking, and a project Java saved with documentation (fixtures/
|
||
// documentation.fls) read, kept in the document and written again.
|
||
|
||
import { readFileSync } from 'node:fs';
|
||
import { dirname, join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { describe, expect, it } from 'vitest';
|
||
import * as Y from 'yjs';
|
||
import {
|
||
DOCUMENTATION_FONT_SIZES,
|
||
DOCUMENTATION_RECTANGLE_COLORS,
|
||
DOCUMENTATION_TEXT_COLORS,
|
||
documentationFontStyle,
|
||
documentationItem,
|
||
inJavaDrawingOrder,
|
||
resizedFrame,
|
||
type DocumentationColor,
|
||
type DocumentationItem,
|
||
} from '@mdg-filius/core';
|
||
import { exportProject, openProjectArchive } from '@mdg-filius/fls';
|
||
import { addDocumentation, documentationText, readProject, removeDocumentation, setDocumentationColor, setDocumentationFont, setDocumentationFrame, topologyView, writeProject } from '../src/index.js';
|
||
|
||
type Rgba = [number, number, number, number];
|
||
interface JavaFont {
|
||
readonly name: string;
|
||
readonly style: number;
|
||
readonly size: number;
|
||
}
|
||
interface JavaElement {
|
||
readonly bounds: [number, number, number, number];
|
||
readonly color: Rgba;
|
||
readonly font: JavaFont | null;
|
||
readonly text: string | null;
|
||
}
|
||
interface JavaItem extends JavaElement {
|
||
readonly type: number;
|
||
}
|
||
interface JavaMenuEntry {
|
||
readonly menu?: string;
|
||
readonly items?: string[];
|
||
readonly colors?: string;
|
||
readonly list?: Rgba[];
|
||
readonly rows?: number;
|
||
readonly item?: string;
|
||
readonly separator?: boolean;
|
||
}
|
||
|
||
const here = dirname(fileURLToPath(import.meta.url));
|
||
const java = JSON.parse(readFileSync(join(here, 'documentation.java.json'), 'utf8')) as {
|
||
readonly new: { readonly text: JavaElement; readonly rectangle: JavaElement };
|
||
readonly fonts: JavaFont[];
|
||
readonly textColor: Rgba;
|
||
readonly rectangleColor: Rgba;
|
||
readonly menus: { readonly text: JavaMenuEntry[]; readonly rectangle: JavaMenuEntry[] };
|
||
readonly smallest: Record<string, JavaElement>;
|
||
readonly stacking: string[];
|
||
readonly topAt: string;
|
||
readonly saved: JavaItem[];
|
||
readonly loaded: JavaItem[];
|
||
};
|
||
|
||
const channel = (value: number) => Math.round(value * 255);
|
||
const rgba = (color: DocumentationColor): Rgba => [channel(color.red), channel(color.green), channel(color.blue), channel(color.alpha)];
|
||
/** An item as Java's GUIDocuItem shows it: its bounds, colour and font (none for a rectangle). */
|
||
const asJava = (item: DocumentationItem) => ({
|
||
bounds: [item.frame.x, item.frame.y, item.frame.width, item.frame.height],
|
||
color: rgba(item.color),
|
||
font: item.kind === 'text' ? { name: item.fontName, style: documentationFontStyle(item), size: item.fontSize } : null,
|
||
text: item.kind === 'text' ? item.text : null,
|
||
});
|
||
const itemOf = (doc: Y.Doc, id: string) => readProject(doc).documentationItems.find((item) => item.id === id)!;
|
||
|
||
describe("the documentation items, against Java's documentation mode", () => {
|
||
it('makes a new text and rectangle as Java does: 100 × 50 where dropped, a text empty and black in Dialog 12, a rectangle grey', () => {
|
||
const doc = new Y.Doc();
|
||
addDocumentation(doc, { id: 'text', kind: 'text', x: 100, y: 100 }, 'me');
|
||
addDocumentation(doc, { id: 'rectangle', kind: 'rectangle', x: 300.4, y: 99.6 }, 'me');
|
||
expect(asJava(itemOf(doc, 'text'))).toEqual(java.new.text);
|
||
expect(asJava(itemOf(doc, 'rectangle'))).toEqual(java.new.rectangle);
|
||
// After the others: of its kind the lowest.
|
||
expect([itemOf(doc, 'text').order, itemOf(doc, 'rectangle').order]).toEqual([0, 1]);
|
||
// An item made without a font has Java's: Dialog 12, plain.
|
||
expect(documentationItem({ id: 'x', kind: 'text', frame: { x: 0, y: 0, width: 5, height: 5 }, color: DOCUMENTATION_TEXT_COLORS[0]!, order: 0 })).toMatchObject({
|
||
fontName: 'Dialog',
|
||
fontSize: 12,
|
||
isBold: false,
|
||
isItalic: false,
|
||
frame: { x: 0, y: 0, width: 5, height: 5 },
|
||
});
|
||
});
|
||
|
||
it("offers Java's colours and font sizes, and sets font and colour as Java's menu does", () => {
|
||
const colors = (entries: JavaMenuEntry[]) => entries.find((entry) => entry.list !== undefined)!;
|
||
expect(DOCUMENTATION_TEXT_COLORS.map(rgba)).toEqual(colors(java.menus.text).list);
|
||
expect(DOCUMENTATION_RECTANGLE_COLORS.map(rgba)).toEqual(colors(java.menus.rectangle).list);
|
||
expect([colors(java.menus.text).rows, colors(java.menus.rectangle).rows]).toEqual([3, 3]);
|
||
expect(java.menus.text.map((entry) => entry.menu ?? entry.colors ?? entry.item ?? '-')).toEqual(['Schriftgröße', 'Schriftstil', 'Farbe', '-', 'Löschen']);
|
||
expect(java.menus.rectangle.map((entry) => entry.menu ?? entry.colors ?? entry.item ?? '-')).toEqual(['Farbe', '-', 'Löschen']);
|
||
|
||
// „Groß“, „Kursiv“, „Fett“, „Normal“, „Sehr Groß“: a style replaces the one before.
|
||
const doc = new Y.Doc();
|
||
addDocumentation(doc, { id: 'text', kind: 'text', x: 100, y: 100 }, 'me');
|
||
const font = () => asJava(itemOf(doc, 'text')).font;
|
||
const fonts = [];
|
||
for (const change of [{ size: DOCUMENTATION_FONT_SIZES[1] }, { style: 'italic' }, { style: 'bold' }, { size: DOCUMENTATION_FONT_SIZES[0] }, { size: DOCUMENTATION_FONT_SIZES[2] }] as const) {
|
||
setDocumentationFont(doc, 'text', change, 'me');
|
||
fonts.push(font());
|
||
}
|
||
expect(fonts).toEqual(java.fonts);
|
||
// The drawing knows the style too.
|
||
setDocumentationFont(doc, 'text', { style: 'italic' }, 'me');
|
||
expect(topologyView(doc).docs.map((item) => [item.bold, item.italic])).toEqual([[false, true]]);
|
||
setDocumentationFont(doc, 'text', { style: 'plain' }, 'me');
|
||
expect(font()?.style).toBe(0);
|
||
setDocumentationColor(doc, 'text', DOCUMENTATION_TEXT_COLORS[7]!, 'me');
|
||
expect(rgba(itemOf(doc, 'text').color)).toEqual(java.textColor);
|
||
|
||
// A rectangle takes a colour, not a font.
|
||
addDocumentation(doc, { id: 'rectangle', kind: 'rectangle', x: 300, y: 100 }, 'me');
|
||
setDocumentationFont(doc, 'rectangle', { size: 24, style: 'bold' }, 'me');
|
||
setDocumentationColor(doc, 'rectangle', DOCUMENTATION_RECTANGLE_COLORS[4]!, 'me');
|
||
expect(asJava(itemOf(doc, 'rectangle'))).toMatchObject({ color: java.rectangleColor, font: null });
|
||
expect([itemOf(doc, 'rectangle').fontSize, itemOf(doc, 'rectangle').isBold]).toEqual([12, false]);
|
||
});
|
||
|
||
it('leaves a rectangle at least 20 × 20 and a text at least 88 × 20, the opposite edge where it was', () => {
|
||
const rectangle = { x: 300, y: 100, width: 100, height: 50 };
|
||
const [x, y, width, height] = java.smallest.rectangle!.bounds;
|
||
expect(resizedFrame(rectangle, 'rectangle', { right: true, bottom: true }, { x: 303, y: 103 })).toEqual({ x, y, width, height });
|
||
// By its top left corner, far past the other: the right and lower edges stay.
|
||
const small = { x: 300, y: 100, width: 20, height: 20 };
|
||
const [ax, ay, aw, ah] = java.smallest.rectangleByItsTopLeft!.bounds;
|
||
expect(resizedFrame(small, 'rectangle', { left: true, top: true }, { x: 500, y: 300 })).toEqual({ x: ax, y: ay, width: aw, height: ah });
|
||
const [tx, ty, tw, th] = java.smallest.textSmallest!.bounds;
|
||
expect(resizedFrame({ x: 100, y: 100, width: 100, height: 50 }, 'text', { right: true, bottom: true }, { x: 0, y: 0 })).toEqual({ x: tx, y: ty, width: tw, height: th });
|
||
// Edges follow the pointer; the others stay.
|
||
expect(resizedFrame(rectangle, 'rectangle', { left: true }, { x: 250, y: 0 })).toEqual({ x: 250, y: 100, width: 150, height: 50 });
|
||
expect(resizedFrame(rectangle, 'rectangle', { top: true }, { x: 0, y: 130 })).toEqual({ x: 300, y: 130, width: 100, height: 20 });
|
||
expect(resizedFrame(rectangle, 'rectangle', { bottom: true }, { x: 999, y: 300 })).toEqual({ x: 300, y: 100, width: 100, height: 200 });
|
||
expect(resizedFrame(rectangle, 'rectangle', {}, { x: 0, y: 0 })).toEqual(rectangle);
|
||
|
||
// The document keeps whole pixels.
|
||
const doc = new Y.Doc();
|
||
addDocumentation(doc, { id: 'r', kind: 'rectangle', x: 0, y: 0 }, 'me');
|
||
setDocumentationFrame(doc, 'r', { x: 10.4, y: 20.6, width: 30.5, height: 40.2 }, 'me');
|
||
expect(itemOf(doc, 'r').frame).toEqual({ x: 10, y: 21, width: 31, height: 40 });
|
||
});
|
||
|
||
it('stacks as Java does: texts over rectangles, of one kind the earlier over the later', () => {
|
||
const doc = new Y.Doc();
|
||
const names = ['text 1', 'rectangle 1', 'text 2', 'rectangle 2'];
|
||
names.forEach((name, index) => addDocumentation(doc, { id: name, kind: name.startsWith('text') ? 'text' : 'rectangle', x: 50 + 10 * index, y: 50 + 10 * index }, 'me'));
|
||
// Drawn in this order, the last on top; Java names the top one first.
|
||
const drawn = topologyView(doc).docs.map((item) => item.id);
|
||
expect([...drawn].reverse()).toEqual(java.stacking);
|
||
expect(drawn.at(-1)).toBe(java.topAt);
|
||
expect(inJavaDrawingOrder(readProject(doc).documentationItems).map((item) => item.id)).toEqual(drawn);
|
||
});
|
||
|
||
it('reads what Java saved, keeps it in the document, and writes it as Java does', () => {
|
||
let counter = 0;
|
||
const project = openProjectArchive(new Uint8Array(readFileSync(join(here, 'fixtures/documentation.fls'))), () => `id-${++counter}`);
|
||
expect(java.loaded).toEqual(java.saved);
|
||
const byFile = (items: readonly DocumentationItem[]) => [...items].sort((lhs, rhs) => lhs.order - rhs.order);
|
||
const javaItems = java.loaded.map(({ type, ...rest }) => ({ ...rest, type }));
|
||
expect(byFile(project.documentationItems).map((item) => ({ ...asJava(item), type: item.kind === 'rectangle' ? 1 : 2 }))).toEqual(javaItems);
|
||
|
||
// Through the document and back: italic and all.
|
||
const doc = new Y.Doc();
|
||
writeProject(doc, project);
|
||
expect(readProject(doc).documentationItems).toEqual(project.documentationItems);
|
||
expect(documentationText(doc, project.documentationItems.find((item) => item.kind === 'text')!.id)).toBeInstanceOf(Y.Text);
|
||
expect(documentationText(doc, project.documentationItems.find((item) => item.kind === 'rectangle')!.id)).toBeUndefined();
|
||
|
||
// Written as Java writes it (rectangles first in the list, which leaves Java's stacking of each kind as it was).
|
||
const written = exportProject(readProject(doc)).topology.documentation.map((item) => ({
|
||
...item,
|
||
color: item.color === null ? null : [...item.color],
|
||
font: item.font === null ? null : { ...item.font },
|
||
}));
|
||
const expected = [...java.saved.filter((item) => item.type === 1), ...java.saved.filter((item) => item.type === 2)].map((item) => ({
|
||
type: item.type,
|
||
text: item.text ?? '',
|
||
x: item.bounds[0],
|
||
y: item.bounds[1],
|
||
width: item.bounds[2],
|
||
height: item.bounds[3],
|
||
color: item.color,
|
||
// A rectangle has no font in Java; the model gives every item one.
|
||
font: item.font ?? { name: 'Dialog', style: 0, size: 12 },
|
||
}));
|
||
expect(written).toEqual(expected);
|
||
|
||
// A colour of Java's menu is opaque, also over a translucent one from a file.
|
||
const translucent = project.documentationItems[0]!;
|
||
setDocumentationColor(doc, translucent.id, { ...translucent.color, alpha: 0.5 }, 'me');
|
||
expect(itemOf(doc, translucent.id).color.alpha).toBe(0.5);
|
||
setDocumentationColor(doc, translucent.id, DOCUMENTATION_RECTANGLE_COLORS[4]!, 'me');
|
||
expect(rgba(itemOf(doc, translucent.id).color)).toEqual([255, 201, 201, 255]);
|
||
|
||
removeDocumentation(doc, project.documentationItems[0]!.id, 'me');
|
||
expect(readProject(doc).documentationItems).toHaveLength(3);
|
||
});
|
||
});
|