Skip to content

Commit

Permalink
feat: improved serialization of text mode
Browse files Browse the repository at this point in the history
  • Loading branch information
arnog committed Dec 5, 2023
1 parent 352b55e commit 4f9dbcc
Show file tree
Hide file tree
Showing 14 changed files with 163 additions and 55 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Bugs Fixed

- Correctly position the menu when the document has been scrolled.
- When serializing, do not generate a `\text` command around a `\texttt` command.

## 0.98.0 (2023-12-03)

Expand Down
6 changes: 4 additions & 2 deletions css/mathfield.less
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
align-items: flex-end;
min-height: 39px; /* Need some room for the virtual keyboard toggle */
width: 100%;

/* Encourage browsers to consider allocating a hardware accelerated
layer for this element. */
isolation: isolate;
Expand Down Expand Up @@ -92,7 +92,7 @@
align-self: center;
position: relative;
overflow: hidden;
padding: 2px 0 2px 1px;
padding: 2px 3px 2px 1px;
width: 100%;
}

Expand Down Expand Up @@ -135,6 +135,8 @@
}




/* The invisible element used to capture keyboard events. We're just trying
really hard to make sure it doesn't show. */
.ML__keyboard-sink {
Expand Down
11 changes: 6 additions & 5 deletions src/core/atom-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,12 @@ export class Atom<T extends (Argument | null)[] = (Argument | null)[]> {
// for operators in `textstyle` style)
// - 'auto': 'over-under' in \displaystyle, 'adjacent' otherwise
// If `undefined`, the subsup should be placed on a separate `subsup` atom.
subsupPlacement: 'auto' | 'over-under' | 'adjacent' | undefined = undefined;
subsupPlacement: 'auto' | 'over-under' | 'adjacent' | undefined;

// True if the subsupPlacement was set by `\limits`, `\nolimits` or
// `\displaylimits`.
// Necessary so the proper LaTeX can be output.
explicitSubsupPlacement = false;
explicitSubsupPlacement: boolean;

// If true, the atom represents a function (which can be followed by
// parentheses) e.g. "f" or "\sin"
Expand Down Expand Up @@ -160,13 +160,14 @@ export class Atom<T extends (Argument | null)[] = (Argument | null)[]> {
if (typeof options.value === 'string') this.value = options.value;
this.command = options.command ?? this.value ?? '';
this.mode = options.mode ?? 'math';
this.isFunction = options.isFunction ?? false;
this.subsupPlacement = options.limits;
if (options.isFunction) this.isFunction = true;
if (options.limits) this.subsupPlacement = options.limits;
this.style = { ...options.style } ?? {};
this.displayContainsHighlight = options.displayContainsHighlight ?? false;
this.captureSelection = options.captureSelection ?? false;
this.skipBoundary = options.skipBoundary ?? false;
this.verbatimLatex = options.verbatimLatex ?? undefined;
if (options.verbatimLatex !== undefined && options.verbatimLatex !== null)
this.verbatimLatex = options.verbatimLatex;
if (options.args) this.args = options.args;
if (options.body) this.body = options.body;
this._changeCounter = 0;
Expand Down
22 changes: 14 additions & 8 deletions src/core/modes-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ function emitSizeTextRun(run: Atom[], options: ToLatexOptions): string[] {
});
}

function emitFontFamilyTextRun(run: Atom[], options: ToLatexOptions): string[] {
function emitFontFamilyTextRun(
run: Atom[],
options: ToLatexOptions,
needsWrap: boolean
): string[] {
return getPropertyRuns(run, 'fontFamily').map((x: Atom[]) => {
const s = emitSizeTextRun(x, options);
const command =
Expand All @@ -85,6 +89,7 @@ function emitFontFamilyTextRun(run: Atom[], options: ToLatexOptions): string[] {
if (x[0].style.fontFamily)
return `{\\fontfamily{${x[0].style.fontFamily}} ${joinLatex(s)}}`;

if (needsWrap) return `\\text{${joinLatex(s)}}`;
return joinLatex(s);
});
}
Expand Down Expand Up @@ -117,13 +122,14 @@ export class TextMode extends Mode {
}

serialize(run: Atom[], options: ToLatexOptions): string[] {
const result = emitFontFamilyTextRun(run, {
...options,
defaultMode: 'text',
});

if (result.length === 0 || options.defaultMode === 'text') return result;
return ['\\text{', ...result, '}'];
return emitFontFamilyTextRun(
run,
{
...options,
defaultMode: 'text',
},
options.defaultMode !== 'text'
);
}

/**
Expand Down
9 changes: 8 additions & 1 deletion src/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1552,8 +1552,13 @@ export class Parser {

// An unknown command, or a command not available in this mode
if (!info) {
this.onError({ code: 'unknown-command', arg: command });
if (this.parseMode === 'text') {
if (/[a-zA-Z]/.test(this.peek() ?? '')) {
// The following character is a letter: insert a space
// i.e. `\alpha x` -> `\alpha~x`
// (the spaces are removed by the tokenizer)
command += ' ';
}
return [...command].map(
(c) =>
new Atom({
Expand All @@ -1564,6 +1569,8 @@ export class Parser {
})
);
}

this.onError({ code: 'unknown-command', arg: command });
return [new ErrorAtom(command)];
}

Expand Down
13 changes: 6 additions & 7 deletions src/core/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,19 @@ import { splitGraphemes } from './grapheme-splitter';
import type { Token } from '../public/core-types';

/**
* Given a LaTeX expression represented as a character string,
* the Lexer class will scan and return Tokens for the lexical
* Given a LaTeX string, the Tokenizer will return Tokens for the lexical
* units in the string.
*
* @param s A string of LaTeX
* @param s A LaTeX string
*/
class Tokenizer {
obeyspaces: boolean;
obeyspaces = false;

private readonly s: string | string[];
private pos: number;
private pos = 0;

constructor(s: string) {
this.s = splitGraphemes(s);
this.pos = 0;
this.obeyspaces = false;
}

/**
Expand Down Expand Up @@ -71,6 +69,7 @@ class Tokenizer {
next(): Token | null {
// If we've reached the end, exit
if (this.end()) return null;

// Handle white space
// In text mode, spaces are significant,
// however they are coalesced unless \obeyspaces
Expand Down
7 changes: 4 additions & 3 deletions src/editor-mathfield/mathfield-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,11 +421,12 @@ If you are using Vue, this may be because you are using the runtime-only build o
);

if (
this.userSelect === 'none' ||
this.model.atoms.length <= 1 ||
this.disabled ||
(this.readOnly && !this.hasEditableContent) ||
this.disabled
this.userSelect === 'none'
)
menuToggle!.style.display = 'none';
menuToggle.style.display = 'none';

this.ariaLiveText = this.element.querySelector('[role=status]')!;
// this.accessibleMathML = this.element.querySelector('.accessibleMathML')!;
Expand Down
2 changes: 2 additions & 0 deletions src/editor-mathfield/mode-editor-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ function convertStringToAtoms(s: string, context: ContextInterface): Atom[] {
s = s.replace(/_/g, '\\_');
s = s.replace(/{/g, '\\textbraceleft ');
s = s.replace(/}/g, '\\textbraceright ');
s = s.replace(/lbrace/g, '\\textbraceleft ');
s = s.replace(/rbrace/g, '\\textbraceright ');
s = s.replace(/\^/g, '\\textasciicircum ');
s = s.replace(/~/g, '\\textasciitilde ');
s = s.replace(/£/g, '\\textsterling ');
Expand Down
26 changes: 20 additions & 6 deletions src/editor-mathfield/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,11 @@ export function render(
// 1. Hide the virtual keyboard toggle if not applicable
//

const toggle = mathfield.element.querySelector<HTMLElement>(
const keyboardToggle = mathfield.element.querySelector<HTMLElement>(
'[part=virtual-keyboard-toggle]'
);
if (toggle) toggle.style.display = mathfield.hasEditableContent ? '' : 'none';
if (keyboardToggle)
keyboardToggle.style.display = mathfield.hasEditableContent ? '' : 'none';

// NVA tries (and fails) to read MathML, so skip it for now
// mathfield.accessibleMathML.innerHTML = mathfield.options.createHTML(
Expand All @@ -195,6 +196,19 @@ export function render(

let content = contentMarkup(mathfield, renderOptions);

const menuToggle =
mathfield.element.querySelector<HTMLElement>('[part=menu-toggle]');
if (menuToggle) {
if (
mathfield.model.atoms.length <= 1 ||
mathfield.disabled ||
(mathfield.readOnly && !mathfield.hasEditableContent) ||
mathfield.userSelect === 'none'
)
menuToggle.style.display = 'none';
else menuToggle.style.display = '';
}

//
// 3. Render the content placeholder, if applicable
//
Expand Down Expand Up @@ -292,10 +306,10 @@ export function renderSelection(
const element = document.createElement('div');
element.classList.add('ML__contains-highlight');
element.style.position = 'absolute';
element.style.left = `${bounds.left}px`;
element.style.top = `${bounds.top}px`;
element.style.width = `${Math.ceil(bounds.right - bounds.left)}px`;
element.style.height = `${Math.ceil(bounds.bottom - bounds.top - 1)}px`;
element.style.left = `${bounds.left + 1}px`;
element.style.top = `${Math.ceil(bounds.top)}px`;
element.style.width = `${Math.ceil(bounds.right - bounds.left + 1)}px`;
element.style.height = `${Math.ceil(bounds.bottom - bounds.top)}px`;
field.insertBefore(element, field.childNodes[0]);
}
}
Expand Down
28 changes: 14 additions & 14 deletions src/editor/default-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,20 +444,6 @@ export function getDefaultMenuItems(mf: _Mathfield): MenuItem[] {
visible: () => mf.isSelectionEditable,
submenu: getVariantSubmenu(mf),
},
{
label: () => localize('menu.accent')!,
id: 'accent',
containerClass: 'menu-container-variant',
visible: () => mf.isSelectionEditable,
submenu: getAccentSubmenu(mf),
},
{
label: () => localize('menu.decoration')!,
id: 'decoration',
containerClass: 'menu-container-variant',
visible: () => mf.isSelectionEditable && getSelectionAtoms(mf).length > 0,
submenu: getDecorationSubmenu(mf),
},
{
label: () => localize('menu.color')!,
id: 'color',
Expand All @@ -474,6 +460,20 @@ export function getDefaultMenuItems(mf: _Mathfield): MenuItem[] {
visible: () => mf.isSelectionEditable,
submenu: getBackgroundColorSubmenu(mf),
},
{
label: () => localize('menu.accent')!,
id: 'accent',
containerClass: 'menu-container-variant',
visible: () => mf.isSelectionEditable,
submenu: getAccentSubmenu(mf),
},
{
label: () => localize('menu.decoration')!,
id: 'decoration',
containerClass: 'menu-container-variant',
visible: () => mf.isSelectionEditable && getSelectionAtoms(mf).length > 0,
submenu: getDecorationSubmenu(mf),
},
{
type: 'divider',
},
Expand Down
4 changes: 4 additions & 0 deletions src/latex-commands/definitions-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,10 @@ const TEXT_SYMBOLS: Record<string, number> = {
'\\}': 0x007d,
'\\textbraceleft': 0x007b,
'\\textbraceright': 0x007d,
'\\lbrace': 0x007b,
'\\rbrace': 0x007d,
'\\lbrack': 0x005b,
'\\rbrack': 0x005d,
'\\nobreakspace': 0x00a0,
'\\ldots': 0x2026,
'\\textellipsis': 0x2026,
Expand Down
81 changes: 76 additions & 5 deletions src/ui/colors/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export type RgbColor = {
a?: number;
};

export type OklchColor = {
L: number; // 0..1
C: number; // 0.. 0.37
h: number; // 0..360
alpha?: number; // 0..1
};

export type HslColor = {
h: number;
s: number;
Expand Down Expand Up @@ -88,15 +95,79 @@ export function rgbToHex(_: RgbColor): string {
export function luma(color: string): number {
const rgb = parseHex(color);
if (!rgb) return 0;
let [r, g, b] = [rgb.r / 255.0, rgb.g / 255.0, rgb.b / 255.0];
const [r, g, b] = [rgb.r / 255.0, rgb.g / 255.0, rgb.b / 255.0];

// Source: https://www.w3.org/TR/WCAG20/#relativeluminancedef

r = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
g = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
b = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
const conv = (n: number) =>
n <= 0.03928 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);

return 0.2126 * conv(r) + 0.7152 * conv(g) + 0.0722 * conv(b);
}

// https://bottosson.github.io/posts/oklab/

export function oklchToOklab(_: OklchColor): LabColor {
const [L, C, h] = [_.L, _.C, _.h];
const hRadians = (h * Math.PI) / 180;
const result: LabColor = {
L,
a: C * Math.cos(hRadians),
b: C * Math.sin(hRadians),
};
if (_.alpha !== undefined) result.alpha = _.alpha;
return result;
}

export function oklabToRgb(_: LabColor): RgbColor {
const [l, a, b] = [_.L, _.a, _.b];
// const y = (l + 0.16) / 1.16;
// const x = a / 1.16 + y;
// const z = y - b / 1.16;
// const r =
// 0.9999999984496163 * x + 0.3963377777753821 * y + 0.2158037572992955 * z;
// const g = 0.9999999939972972 * y + -0.105561345615017 * z;
// const b2 =
// 1.0000000088817607 * x + 2.03211193885992 * y + -0.5226657980972148 * z;

const L = Math.pow(
l * 0.9999999984505198 + 0.39633779217376786 * a + 0.2158037580607588 * b,
3
);
const M = Math.pow(
l * 1.00000000888176 - 0.10556134232365635 * a - 0.0638541747717059 * b,
3
);
const S = Math.pow(
l * 1.000000054672411 - 0.0894841820949657 * a - 1.2914855378640917 * b,
3
);

const r =
+4.076741661347994 * L - 3.307711590408193 * M + 0.230969928729428 * S;
const g =
-1.2684380040921763 * L + 2.6097574006633715 * M - 0.3413193963102197 * S;
const bl =
-0.004196086541837188 * L - 0.7034186144594493 * M + 1.7076147009309444 * S;

const conv = (n: number) => {
const abs = Math.abs(n);
if (abs > 0.0031308)
return (Math.sign(n) || 1) * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);

return n * 12.92;
};

return {
r: clampByte(conv(r) * 255),
g: clampByte(conv(g) * 255),
b: clampByte(conv(bl) * 255),
a: _.alpha,
};
}

return 0.2126 * r + 0.7152 * g + 0.0722 * b;
export function oklchToRgb(_: OklchColor): RgbColor {
return oklabToRgb(oklchToOklab(_));
}

function hueToRgbChannel(t1: number, t2: number, hue: number): number {
Expand Down
Loading

0 comments on commit 4f9dbcc

Please sign in to comment.