desmost 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/compiler/compile.d.ts +7 -3
  2. package/dist/compiler/compile.d.ts.map +1 -1
  3. package/dist/compiler/compile.js +7 -3
  4. package/dist/compiler/compile.js.map +1 -1
  5. package/dist/compiler/evaluate.d.ts +5 -5
  6. package/dist/compiler/evaluate.d.ts.map +1 -1
  7. package/dist/compiler/evaluate.js +16 -8
  8. package/dist/compiler/evaluate.js.map +1 -1
  9. package/dist/compiler/format.d.ts +2 -2
  10. package/dist/compiler/format.d.ts.map +1 -1
  11. package/dist/compiler/format.js +16 -2
  12. package/dist/compiler/format.js.map +1 -1
  13. package/dist/compiler/index.d.ts +0 -1
  14. package/dist/compiler/index.d.ts.map +1 -1
  15. package/dist/decompiler/decompile.d.ts +13 -2
  16. package/dist/decompiler/decompile.d.ts.map +1 -1
  17. package/dist/decompiler/decompile.js +17 -3
  18. package/dist/decompiler/decompile.js.map +1 -1
  19. package/dist/decompiler/emit.d.ts.map +1 -1
  20. package/dist/decompiler/emit.js +13 -6
  21. package/dist/decompiler/emit.js.map +1 -1
  22. package/dist/decompiler/extract.d.ts.map +1 -1
  23. package/dist/decompiler/extract.js +3 -2
  24. package/dist/decompiler/extract.js.map +1 -1
  25. package/dist/decompiler/format.d.ts.map +1 -1
  26. package/dist/decompiler/format.js +2 -1
  27. package/dist/decompiler/format.js.map +1 -1
  28. package/dist/desmos.d.ts +4 -0
  29. package/dist/desmos.d.ts.map +1 -1
  30. package/dist/desmos.js +6 -0
  31. package/dist/desmos.js.map +1 -1
  32. package/dist/errors.d.ts +18 -0
  33. package/dist/errors.d.ts.map +1 -1
  34. package/dist/errors.js +37 -0
  35. package/dist/errors.js.map +1 -1
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.internal.d.ts +1 -1
  39. package/dist/index.internal.d.ts.map +1 -1
  40. package/dist/index.internal.js +1 -1
  41. package/dist/index.internal.js.map +1 -1
  42. package/dist/index.js +0 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/magic/expr/latex.d.ts +1 -2
  45. package/dist/magic/expr/latex.d.ts.map +1 -1
  46. package/dist/magic/expr/latex.js +1 -4
  47. package/dist/magic/expr/latex.js.map +1 -1
  48. package/dist/magic/expr/text.d.ts +1 -1
  49. package/dist/magic/expr/text.d.ts.map +1 -1
  50. package/dist/magic/expr/text.js +1 -1
  51. package/dist/magic/expr/text.js.map +1 -1
  52. package/dist/magic/global/dark-mode.d.ts.map +1 -1
  53. package/dist/magic/global/dark-mode.js +1 -3
  54. package/dist/magic/global/dark-mode.js.map +1 -1
  55. package/dist/magic/global/desmos.d.ts +4 -1
  56. package/dist/magic/global/desmos.d.ts.map +1 -1
  57. package/dist/magic/global/desmos.js +14 -1
  58. package/dist/magic/global/desmos.js.map +1 -1
  59. package/dist/magic/global/viewport.d.ts +3 -0
  60. package/dist/magic/global/viewport.d.ts.map +1 -1
  61. package/dist/magic/global/viewport.js +23 -0
  62. package/dist/magic/global/viewport.js.map +1 -1
  63. package/dist/magic/incantation.d.ts +13 -8
  64. package/dist/magic/incantation.d.ts.map +1 -1
  65. package/dist/magic/incantation.js +23 -3
  66. package/dist/magic/incantation.js.map +1 -1
  67. package/dist/magic/local/anim.d.ts.map +1 -1
  68. package/dist/magic/local/anim.js +0 -1
  69. package/dist/magic/local/anim.js.map +1 -1
  70. package/dist/magic/local/colour.d.ts +3 -2
  71. package/dist/magic/local/colour.d.ts.map +1 -1
  72. package/dist/magic/local/colour.js +3 -3
  73. package/dist/magic/local/colour.js.map +1 -1
  74. package/dist/magic/local/label.d.ts +2 -1
  75. package/dist/magic/local/label.d.ts.map +1 -1
  76. package/dist/magic/local/label.js +36 -16
  77. package/dist/magic/local/label.js.map +1 -1
  78. package/dist/magic/local/line.d.ts +3 -0
  79. package/dist/magic/local/line.d.ts.map +1 -1
  80. package/dist/magic/local/line.js +10 -0
  81. package/dist/magic/local/line.js.map +1 -1
  82. package/dist/magic/local/slider.d.ts +2 -2
  83. package/dist/magic/local/slider.d.ts.map +1 -1
  84. package/dist/magic/local/slider.js +0 -1
  85. package/dist/magic/local/slider.js.map +1 -1
  86. package/dist/{compiler/options.d.ts → options.d.ts} +51 -22
  87. package/dist/options.d.ts.map +1 -0
  88. package/dist/{compiler/options.js → options.js} +6 -1
  89. package/dist/options.js.map +1 -0
  90. package/dist/parser/desmost-parser.d.ts +1 -1
  91. package/dist/parser/desmost-parser.d.ts.map +1 -1
  92. package/dist/parser/desmost-parser.js +9 -5
  93. package/dist/parser/desmost-parser.js.map +1 -1
  94. package/dist/parser/generic-parser.d.ts +1 -5
  95. package/dist/parser/generic-parser.d.ts.map +1 -1
  96. package/dist/parser/generic-parser.js +7 -10
  97. package/dist/parser/generic-parser.js.map +1 -1
  98. package/dist/utils.d.ts +1 -0
  99. package/dist/utils.d.ts.map +1 -1
  100. package/dist/utils.js +1 -0
  101. package/dist/utils.js.map +1 -1
  102. package/package.json +8 -3
  103. package/src/compiler/compile.ts +8 -4
  104. package/src/compiler/evaluate.ts +22 -13
  105. package/src/compiler/format.ts +22 -4
  106. package/src/compiler/index.ts +0 -1
  107. package/src/decompiler/decompile.ts +25 -3
  108. package/src/decompiler/emit.ts +15 -6
  109. package/src/decompiler/extract.ts +3 -2
  110. package/src/decompiler/format.ts +2 -1
  111. package/src/decompiler/readme.md +9 -1
  112. package/src/desmos.ts +9 -0
  113. package/src/errors.ts +29 -8
  114. package/src/index.internal.ts +1 -1
  115. package/src/index.ts +2 -2
  116. package/src/magic/.template.ts +25 -16
  117. package/src/magic/expr/latex.ts +2 -7
  118. package/src/magic/expr/text.ts +2 -2
  119. package/src/magic/global/dark-mode.ts +1 -3
  120. package/src/magic/global/desmos.ts +23 -1
  121. package/src/magic/global/viewport.ts +40 -1
  122. package/src/magic/incantation.ts +59 -22
  123. package/src/magic/local/anim.ts +0 -1
  124. package/src/magic/local/colour.ts +4 -3
  125. package/src/magic/local/label.ts +49 -20
  126. package/src/magic/local/line.ts +20 -3
  127. package/src/magic/local/slider.ts +2 -3
  128. package/src/{compiler/options.ts → options.ts} +63 -14
  129. package/src/parser/desmost-parser.ts +12 -6
  130. package/src/parser/generic-parser.ts +12 -12
  131. package/src/parser/readme.md +2 -0
  132. package/src/utils.ts +3 -0
  133. package/svelte/desmost.svelte +5 -3
  134. package/dist/compiler/options.d.ts.map +0 -1
  135. package/dist/compiler/options.js.map +0 -1
@@ -1,18 +1,29 @@
1
- import type { DesmostOptions } from "./options";
1
+ import type { DesmostOptions } from "../options";
2
2
 
3
3
  import { UnrecoverableError } from "../errors";
4
+ import { LINE } from "../utils";
4
5
 
5
6
 
6
7
  export function format_error(
7
8
  e: UnrecoverableError,
8
- options: Required<DesmostOptions>,
9
+ options: DesmostOptions,
9
10
  ): string
10
11
  {
11
12
  let prefix = options.error_prefix;
12
13
  let sep = (prefix === "" || prefix.endsWith("\n")) ? "" : " ";
13
14
 
14
- return `${prefix}${sep}${e.name}: ${e.message}`;
15
15
  // TODO maybe include stack? (configurable?)
16
+ let display = `${prefix}${sep}${e.name}: ${e.message}`;
17
+
18
+ if (e.info != undefined && options.expand_errors) {
19
+ display += `\n${LINE}`;
20
+
21
+ if (e.info.hint != undefined) display += `\nHint: ${e.info.hint}`;
22
+ if (e.info.note != undefined) display += `\n${LINE}\nNote: ${e.info.note}`;
23
+ if (e.info.flagged_by != undefined) display += `\n${LINE}\nFlagged by: \`${e.info.flagged_by}\``;
24
+ }
25
+
26
+ return display;
16
27
  }
17
28
 
18
29
 
@@ -37,11 +48,18 @@ export function normalise_latex(latex: string): string
37
48
  export function prettify_latex(latex: string): string
38
49
  {
39
50
  latex = latex.replaceAll(/(?<!\\left)(\(|\[|\\\{)/g, "\\left$1");
51
+ latex = latex.replaceAll(/(?:\\left\s?)+/g, "\\left");
40
52
  latex = latex.replaceAll(/(?<!\\right)(\)|\]|\\\})/g, "\\right$1");
53
+ latex = latex.replaceAll(/(?:\\right\s?)+/g, "\\right");
41
54
  latex = latex.replaceAll(/(:|,)(?!\\ ) */g, "$1\\ ");
42
55
 
43
56
  latex = latex.replaceAll(
44
- /(?<=[^\w]|^)\\?(mean|median|count|total|repeat|join|sort|shuffle|unique|mod|ceil|floor|round|sign) ?(?=\(|\\left\()/g,
57
+ /(?<=[^\w]|^)\\?(length|mean|median|count|total|repeat|join|sort|shuffle|unique|mod|ceil|floor|round|sign) ?(?=\(|\\left\()/g,
58
+ "\\operatorname{$1}"
59
+ );
60
+
61
+ latex = latex.replaceAll(
62
+ /\s(and|or)\s/g,
45
63
  "\\operatorname{$1}"
46
64
  );
47
65
 
@@ -1,2 +1 @@
1
1
  export { compile } from "./compile";
2
- export type { DesmostOptions } from "./options";
@@ -1,7 +1,9 @@
1
1
  import { emit_global_incantation, emit_expression } from "./emit";
2
2
  import { extract_settings, extract_viewport, extract_expression } from "./extract";
3
3
 
4
+ import { fill_defaults } from "../options";
4
5
  import { Ast } from "../parser";
6
+ import type { DesmostOptions } from "../options";
5
7
 
6
8
 
7
9
  export interface SemiStructuredAst
@@ -16,15 +18,35 @@ export interface SemiStructuredAst
16
18
  *
17
19
  * The caller must also supply a `blank` calculator instance. This is used as a reference for default field values, so that emitted incantations like `/desmos{}` don't get filled with a huge amount of noise.
18
20
  */
19
- export function decompile(desmos: Desmos.Calculator, blank: Desmos.Calculator): string
21
+ export function decompile(
22
+ /** The Desmos calculator instance to decompile. */
23
+ desmos: Desmos.Calculator,
24
+
25
+ /**
26
+ * The default blank Desmos calculator instance to use as a baseline.
27
+ *
28
+ * It is **highly recommended** to include this, otherwise your decompiled source will have a bunch of unnecessary incantations!
29
+ *
30
+ * This is used in deciding what settings are noise that can be omitted. For instance, most of your calculator settings are probably set to their default values, so there's no need to include *every single one of them* in `/desmos{}`.
31
+ */
32
+ blank?: Desmos.Calculator,
33
+
34
+ // TODO doc
35
+ options?: Partial<DesmostOptions>,
36
+ ): string
20
37
  {
21
- return ast_to_source(desmos_to_ast(desmos, blank));
38
+ let opts = fill_defaults(options);
39
+ return ast_to_source(desmos_to_ast(desmos, blank ?? desmos));
22
40
  }
23
41
 
24
42
  /**
25
43
  * Decompile `desmos` into a semi-structured AST.
26
44
  */
27
- export function desmos_to_ast(desmos: Desmos.Calculator, blank: Desmos.Calculator): SemiStructuredAst
45
+ export function desmos_to_ast(
46
+ desmos: Desmos.Calculator,
47
+ blank: Desmos.Calculator,
48
+ options: DesmostOptions,
49
+ ): SemiStructuredAst
28
50
  {
29
51
  return {
30
52
  globals: [
@@ -31,15 +31,24 @@ export function emit_expression(expr?: Ast.Expression): string
31
31
 
32
32
  switch (expr.data.type) {
33
33
  case "text":
34
- content = `/text{ ${expr.data.text} }`;
34
+ expr.data.text ??= "";
35
+
36
+ if (expr.data.text.includes("\n")) {
37
+ content = `/text{\n${expr.data.text}\n}`;
38
+ } else {
39
+ content = `% ${expr.data.text}`;
40
+ }
35
41
  break;
42
+
43
+ case "table":
44
+ content = "[tables are currently unsupported]";
45
+ break;
46
+
36
47
  default:
37
- // @ts-expect-error: outdated types
38
- if (expr.data.latex?.trim() === "") {
39
- content = "";
48
+ if (expr.data.latex?.trim() !== "") {
49
+ content = expr.data.latex ?? "[unsupported]";
40
50
  } else {
41
- // @ts-expect-error: outdated types
42
- content = expr.data.latex ?? "?";
51
+ content = "";
43
52
  }
44
53
  }
45
54
 
@@ -1,6 +1,7 @@
1
1
  import { stringify_json5, prettify_source } from "./format";
2
2
 
3
3
  import { Ast } from "../parser";
4
+ import { is_latex } from "../desmos";
4
5
 
5
6
  import { DesmosIncantation, ViewportIncantation } from "../magic/global";
6
7
 
@@ -69,7 +70,7 @@ export function extract_expression(expression: Desmos.ExpressionState): Ast.Expr
69
70
  {
70
71
  let data;
71
72
 
72
- // @ts-expect-error: outdated types
73
+ // @ts-expect-error: check type-narrows
73
74
  if (expression.latex?.trim() === "") {
74
75
  data = { latex: ` ` };
75
76
  } else {
@@ -79,7 +80,7 @@ export function extract_expression(expression: Desmos.ExpressionState): Ast.Expr
79
80
  );
80
81
  }
81
82
 
82
- if ("latex" in data) {
83
+ if (is_latex(expression)) {
83
84
  data.latex = prettify_source(data.latex);
84
85
  }
85
86
 
@@ -3,7 +3,7 @@ export const stringify_json5: (typeof JSON.stringify) = (
3
3
  ) =>
4
4
  {
5
5
  return (
6
- /// @ts-expect-error: random `null`??
6
+ // @ts-expect-error: random `null`??
7
7
  JSON.stringify(data, replacer, space)
8
8
  .slice(1, -1) // strip surrounding {}
9
9
  .trim()
@@ -15,6 +15,7 @@ export const stringify_json5: (typeof JSON.stringify) = (
15
15
  export function prettify_source(source: string): string
16
16
  {
17
17
  source = source.replaceAll(/(?<=[^ ])=(?=[^ ])/g, " = ");
18
+ source = source.replaceAll(/\}\\/g, "} \\")
18
19
 
19
20
  return source;
20
21
  }
@@ -2,7 +2,15 @@
2
2
 
3
3
  This module implements the Desmos -> Desmost decompiler.
4
4
 
5
- This decompiles a `Desmos.Calculator` instance into the *minimal* raw Desmost source code that could *completely* reproduce the calculator state. Decompilation happens through several lowering stages:
5
+ This decompiles a `Desmos.Calculator` instance into the *minimal* raw Desmost source code that could *completely* reproduce the calculator state. In other words, we want this to hold:[^equality]
6
+
7
+ [^equality]: Using value equality of ‘calculator state’.
8
+
9
+ ```ts
10
+ compile(decompile(calc)) == calc
11
+ ```
12
+
13
+ Decompilation happens through several lowering stages:
6
14
 
7
15
  ```hs
8
16
  Desmos.Calculator -> object[] -> Ast[] -> string
package/src/desmos.ts CHANGED
@@ -11,3 +11,12 @@ export enum DesmosColour
11
11
  ORANGE = "#fa7e19",
12
12
  BLACK = "#000000",
13
13
  }
14
+
15
+
16
+ /**
17
+ * Is `expr` a Desmos LaTeX expression (as opposed to a note or table)?
18
+ */
19
+ export function is_latex(expr: Desmos.ExpressionState): expr is Desmos.Expression
20
+ {
21
+ return (expr.type === undefined || expr.type === "expression");
22
+ }
package/src/errors.ts CHANGED
@@ -10,28 +10,49 @@ export type NoMatch = typeof NO_MATCH & { readonly __brand?: unique symbol };
10
10
  *
11
11
  * It's not *literally* unrecoverable in the sense that it will crash the compiler completely, but it means there are no further fallbacks to try.
12
12
  */
13
- export class UnrecoverableError extends Error {}
13
+ export class UnrecoverableError extends Error
14
+ {
15
+ info?: UnrecoverableError.Info;
16
+ }
14
17
 
15
18
  export namespace UnrecoverableError
16
19
  {
20
+ export interface Info
21
+ {
22
+ hint?: string,
23
+ note?: string,
24
+ flagged_by?: string,
25
+ }
26
+
17
27
  /** The parser unexpectedly reached the end of its entire input source code. */
18
- export class UnexpectedEnd extends UnrecoverableError {}
28
+ export class UnexpectedEnd extends UnrecoverableError {
29
+ constructor(msg: string, override info?: Info) { super(msg); this.name = "Unexpected End"; }
30
+ }
19
31
 
20
32
  /** The parser did not receive input that it expected. */
21
- export class MissingInput extends UnrecoverableError {}
33
+ export class MissingInput extends UnrecoverableError {
34
+ constructor(msg: string, override info?: Info) { super(msg); this.name = "Missing Input"; }
35
+ }
22
36
 
23
37
  /** The parser received input that did not match what it expected. */
24
- export class UnexpectedInput extends UnrecoverableError {}
38
+ export class UnexpectedInput extends UnrecoverableError {
39
+ constructor(msg: string, override info?: Info) { super(msg); this.name = "Unexpected Input"; }
40
+ }
25
41
 
26
42
  /** The parser received excess input that it did not expect. */
27
- export class ExcessInput extends UnrecoverableError {}
28
-
43
+ export class ExcessInput extends UnrecoverableError {
44
+ constructor(msg: string, override info?: Info) { super(msg); this.name = "Excess Input"; }
45
+ }
29
46
 
30
47
  /** An incantation can't be applied here. */
31
- export class IllegalIncantation extends UnrecoverableError {}
48
+ export class IllegalIncantation extends UnrecoverableError {
49
+ constructor(msg: string, override info?: Info) { super(msg); this.name = "Illegal Incantation"; }
50
+ }
32
51
 
33
52
  /** An incantation's argument couldn't be parsed. */
34
- export class InvalidArgument extends UnrecoverableError {}
53
+ export class InvalidArgument extends UnrecoverableError {
54
+ constructor(msg: string, override info?: Info) { super(msg); this.name = "Invalid Argument"; }
55
+ }
35
56
  }
36
57
 
37
58
 
@@ -1,4 +1,4 @@
1
- export { fill_defaults, DEFAULT_OPTIONS } from "./compiler/options";
1
+ export { DEFAULT_OPTIONS } from "./options";
2
2
  export type { DesmostDebug } from "./compiler/compile";
3
3
 
4
4
  export { Ast } from "./parser";
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { compile, type DesmostOptions } from "./compiler";
2
- export { decompile } from "./decompiler";
1
+ export { compile } from "./compiler";
2
+ export type { DesmostOptions } from "./options";
@@ -1,22 +1,31 @@
1
1
  // import { Incantation, ArgIncantation, type GLOBAL, type LOCAL, type EXPR } from "../incantation";
2
2
 
3
+ // import type { DesmostOptions } from "../../options";
4
+ // import { UnrecoverableError, type Unrecoverable } from "../../errors";
5
+
3
6
 
4
7
  // export class ?Incantation extends ArgIncantation<?>
5
8
  // {
6
- // override readonly description
7
- // = ""
8
-
9
- // override readonly identifier = ?
10
- // override readonly requires_arg = ?
11
- // override readonly arg_type = Incantation.ArgType.?
12
-
13
- // override apply(target: ?, data: ?)
14
- // {
15
- // // TODO
16
- // }
17
-
18
- // override evaluate_arg(raw: ?): ?
19
- // {
20
- // // TODO
21
- // }
9
+ // override readonly description
10
+ // = ""
11
+
12
+ // override readonly identifier = ?
13
+ // override readonly requires_arg = ?
14
+ // override readonly arg_type = Incantation.ArgType.?
15
+
16
+ // override apply(target: ?, data: ?)
17
+ // {
18
+ // // TODO
19
+ // }
20
+
21
+ // override evaluate_arg(raw: ?, options: DesmostOptions): Unrecoverable<?>
22
+ // {
23
+ // let out = super.evaluate_arg(raw, options) as ?;
24
+
25
+ // if (options.check_args) {
26
+ // // TODO
27
+ // }
28
+
29
+ // return out;
30
+ // }
22
31
  // }
@@ -10,15 +10,10 @@ export class LatexIncantation extends ArgIncantation<EXPR>
10
10
  override readonly requires_arg = true
11
11
  override readonly arg_type = Incantation.ArgType.LATEX
12
12
 
13
- apply(target: Desmos.ExpressionState, data: string): void
13
+ apply(target: Desmos.ExpressionState, data: string): asserts target is Desmos.Expression
14
14
  {
15
15
  target.type = "expression";
16
- // @ts-expect-error: outdated types
16
+ // @ts-expect-error: previous assignment type-narrows
17
17
  target.latex = data === "" ? " " : data;
18
18
  }
19
-
20
- override evaluate_arg(data: string): string
21
- {
22
- return data;
23
- }
24
19
  }
@@ -11,10 +11,10 @@ export class TextIncantation extends ArgIncantation<EXPR>
11
11
  override readonly requires_arg = true
12
12
  override readonly arg_type = Incantation.ArgType.STRING
13
13
 
14
- override apply(target: Desmos.ExpressionState, data: string)
14
+ override apply(target: Desmos.ExpressionState, data: string): asserts target is Desmos.Note
15
15
  {
16
16
  target.type = "text";
17
- // @ts-expect-error: outdated types
17
+ // @ts-expect-error: previous assignment type-narrows
18
18
  target.text = data;
19
19
  }
20
20
  }
@@ -10,8 +10,6 @@ export class DarkModeIncantation extends Incantation<GLOBAL>
10
10
 
11
11
  apply(target: Desmos.Calculator)
12
12
  {
13
- target.updateSettings({
14
- invertedColors: true,
15
- });
13
+ target.updateSettings({ invertedColors: true });
16
14
  }
17
15
  }
@@ -1,5 +1,8 @@
1
1
  import { Incantation, ArgIncantation, type GLOBAL } from "../incantation";
2
2
 
3
+ import { UnrecoverableError, type Unrecoverable } from "../../errors";
4
+ import type { DesmostOptions } from "../../options";
5
+
3
6
 
4
7
  type DesmosSettings = Desmos.GraphConfiguration & Desmos.GraphSettings
5
8
 
@@ -7,7 +10,7 @@ type DesmosSettings = Desmos.GraphConfiguration & Desmos.GraphSettings
7
10
  export class DesmosIncantation extends ArgIncantation<GLOBAL>
8
11
  {
9
12
  override readonly description
10
- = "Set the configuration of the calculator via `Calculator.updateSettings()`."
13
+ = "Tweak calculator settings via `Calculator.updateSettings()`."
11
14
 
12
15
  override readonly identifier = "desmos"
13
16
  override readonly requires_arg = false
@@ -22,4 +25,23 @@ export class DesmosIncantation extends ArgIncantation<GLOBAL>
22
25
  target.updateSettings(data);
23
26
  }
24
27
  }
28
+
29
+ override evaluate_arg(raw: string, options: DesmostOptions): Unrecoverable<DesmosSettings>
30
+ {
31
+ let out = super.evaluate_arg(raw, options) as DesmosSettings;
32
+
33
+ if (options.check_args) {
34
+ if (Object.keys(out).length === 0) {
35
+ throw new UnrecoverableError.MissingInput(
36
+ `/${this.identifier} received empty settings`,
37
+ {
38
+ hint: `Provide settings like \`/desmos{expressionsCollapsed: true}\``,
39
+ note: `Use just /desmos (without \`{}\`) if you want it as an indicator`,
40
+ }
41
+ );
42
+ }
43
+ }
44
+
45
+ return out;
46
+ }
25
47
  }
@@ -1,5 +1,8 @@
1
1
  import { Incantation, ArgIncantation, type GLOBAL } from "../incantation";
2
2
 
3
+ import { UnrecoverableError, type Unrecoverable } from "../../errors";
4
+ import type { DesmostOptions } from "../../options";
5
+
3
6
 
4
7
  interface ViewportBounds
5
8
  {
@@ -7,6 +10,8 @@ interface ViewportBounds
7
10
  bottom?: number; top?: number;
8
11
  }
9
12
 
13
+ const VALID_KEYS = ["left", "right", "bottom", "top"];
14
+
10
15
 
11
16
  export class ViewportIncantation extends ArgIncantation<GLOBAL>
12
17
  {
@@ -17,7 +22,7 @@ export class ViewportIncantation extends ArgIncantation<GLOBAL>
17
22
  override readonly requires_arg = true
18
23
  override readonly arg_type = Incantation.ArgType.OBJECT
19
24
 
20
- apply(target: Desmos.Calculator, data: ViewportBounds)
25
+ override apply(target: Desmos.Calculator, data: ViewportBounds)
21
26
  {
22
27
  let existing = target.graphpaperBounds.mathCoordinates;
23
28
 
@@ -30,4 +35,38 @@ export class ViewportIncantation extends ArgIncantation<GLOBAL>
30
35
 
31
36
  target.setMathBounds({ left, right, bottom, top });
32
37
  }
38
+
39
+ override evaluate_arg(raw: string, options: DesmostOptions): Unrecoverable<ViewportBounds>
40
+ {
41
+ let out = super.evaluate_arg(raw, options) as ViewportBounds;
42
+
43
+ if (options.check_args) {
44
+ if (Object.keys(out).length === 0) {
45
+ throw new UnrecoverableError.InvalidArgument(
46
+ `/${this.identifier} received empty viewport bounds`,
47
+ {
48
+ hint: `Provide bounds like \`/viewport{left: -8, right: 8}\``,
49
+ note: `Missing bounds are kept as their defaults`,
50
+ flagged_by: "check_args",
51
+ }
52
+ );
53
+ }
54
+
55
+ let invalid_keys = Object.keys(out).filter(key => !VALID_KEYS.includes(key));
56
+
57
+ if (invalid_keys.length > 0) {
58
+ let plural = invalid_keys.length > 1 ? "fields" : "field";
59
+
60
+ throw new UnrecoverableError.InvalidArgument(
61
+ `/${this.identifier} received invalid ${plural}: [${invalid_keys.join(", ")}]`,
62
+ {
63
+ hint: `Valid fields are: [${VALID_KEYS.join(", ")}]`,
64
+ flagged_by: "check_args",
65
+ }
66
+ );
67
+ }
68
+ }
69
+
70
+ return out;
71
+ }
33
72
  }
@@ -1,5 +1,7 @@
1
1
  import Json5 from "json5";
2
+ import dedent from "dedent";
2
3
 
4
+ import type { DesmostOptions } from "../options";
3
5
  import { UnrecoverableError, type Unrecoverable } from "../errors";
4
6
 
5
7
 
@@ -21,17 +23,17 @@ export abstract class Incantation<
21
23
  >
22
24
  {
23
25
  /** Short user-facing description of what the incantation does. */
24
- public abstract readonly description: string
26
+ abstract readonly description: string
25
27
 
26
28
  /** The raw text sequence that matches this incantation, such as `viewport` or `hidden`. */
27
- public abstract readonly identifier: string
29
+ abstract readonly identifier: string
28
30
 
29
31
  /** An alternative `.identifier`, strictly for localisation purposes only. */
30
- public readonly alias?: string
32
+ readonly alias?: string
31
33
 
32
34
 
33
35
  /** Apply this incantation's effect to `target`, using the provided `data` if required. */
34
- public abstract apply(
36
+ abstract apply(
35
37
  target: Effect extends GLOBAL ? Desmos.Calculator : Desmos.ExpressionState,
36
38
  data?: unknown,
37
39
  ): void
@@ -45,7 +47,7 @@ export abstract class Incantation<
45
47
  if ((actual ?? "expression") !== required)
46
48
  {
47
49
  throw new UnrecoverableError.IllegalIncantation(
48
- `/${this.identifier} can only applied to ${required === "expression" ? "latex" : required} expressions, but target block has type: ${actual}`
50
+ `/${this.identifier} can only applied to ${required === "expression" ? "latex" : required} blocks, but target block has type: ${actual}`
49
51
  );
50
52
  }
51
53
  }
@@ -61,10 +63,10 @@ export abstract class ArgIncantation<
61
63
  extends Incantation<Effect>
62
64
  {
63
65
  /** Does this incantation always require an argument to be passed? */
64
- public readonly requires_arg: boolean = true
66
+ readonly requires_arg: boolean = true
65
67
 
66
68
  /** What type of argument does this incantation accept? */
67
- public abstract readonly arg_type: Incantation.ArgType
69
+ abstract readonly arg_type: Incantation.ArgType
68
70
 
69
71
 
70
72
  /**
@@ -75,14 +77,18 @@ export abstract class ArgIncantation<
75
77
  * ## Notes
76
78
  *
77
79
  * Child incantation classes should override this method, though the defaults should cover most cases (except `ArgType.ENUM`).
80
+ *
81
+ * Validity checks for missing or invalid values, incorrect types, and unknown fields should be performed here, so that `.apply()` can assume the data it receives is guaranteed to be valid.
78
82
  */
79
- public evaluate_arg(raw: string): Unrecoverable<unknown>
83
+ evaluate_arg(raw: string, options: DesmostOptions): Unrecoverable<unknown>
80
84
  {
81
85
  switch (this.arg_type) {
82
- case Incantation.ArgType.STRING: return raw;
83
86
  case Incantation.ArgType.LATEX: return raw;
84
87
  case Incantation.ArgType.ENUM: return raw;
85
88
 
89
+ case Incantation.ArgType.STRING:
90
+ return (options.dedent_text) ? dedent(raw): raw.trim();
91
+
86
92
  case Incantation.ArgType.OBJECT: {
87
93
  try {
88
94
  return Json5.parse(`{${raw}}`);
@@ -94,9 +100,53 @@ export abstract class ArgIncantation<
94
100
  }
95
101
  }
96
102
  }
103
+
104
+ protected require_nonempty(
105
+ arg: object,
106
+ msg?: string,
107
+ info?: UnrecoverableError.Info,
108
+ ): Unrecoverable<void>
109
+ {
110
+ if (Object.entries(arg).length === 0) {
111
+ throw new UnrecoverableError.MissingInput(
112
+ msg ?? `/${this.identifier} received an empty argument {}`,
113
+ info,
114
+ );
115
+ }
116
+ }
117
+
118
+ protected require_known(
119
+ arg: object,
120
+ valid_fields: string[],
121
+ info?: UnrecoverableError.Info,
122
+ ): Unrecoverable<void>
123
+ {
124
+ // TODO accumulate
125
+ for (let field of Object.keys(arg)) {
126
+ if (!valid_fields.includes(field)) {
127
+ throw new UnrecoverableError.InvalidArgument(
128
+ `/${this.identifier} received invalid field: ${field}`,
129
+ {
130
+ hint: `Valid fields are [${valid_fields.join(", ")}]`,
131
+ ...info,
132
+ }
133
+ );
134
+ }
135
+ }
136
+ }
97
137
  }
98
138
 
99
139
 
140
+ /** An incantation that affects the entire Desmos calculator state, like `/desmos` or `/viewport`. */
141
+ export type GLOBAL = Incantation.Effect.GLOBAL;
142
+
143
+ /** An incantation that affects only the expression immediately following it, like `/hide` or `/slider`. */
144
+ export type LOCAL = Incantation.Effect.LOCAL;
145
+
146
+ /** An incantation that produces an expression, like `/latex` or `/text`. */
147
+ export type EXPR = Incantation.Effect.EXPR;
148
+
149
+
100
150
  export namespace Incantation
101
151
  {
102
152
  /** The kind of effect an incantation produces - it either modifies only one block, or modifies the calculator as a whole. */
@@ -128,16 +178,3 @@ export namespace Incantation
128
178
  OBJECT = "Object",
129
179
  }
130
180
  }
131
-
132
-
133
- // == ALIASES == //
134
-
135
- /** An incantation that affects the entire Desmos calculator state, like `/desmos` or `/viewport`. */
136
- export type GLOBAL = Incantation.Effect.GLOBAL;
137
-
138
- /** An incantation that affects only the expression immediately following it, like `/hide` or `/slider`. */
139
- export type LOCAL = Incantation.Effect.LOCAL;
140
-
141
- /** An incantation that produces an expression, like `/latex` or `/text`. */
142
- export type EXPR = Incantation.Effect.EXPR;
143
-
@@ -11,7 +11,6 @@ export class AnimIncantation extends Incantation<LOCAL>
11
11
  override apply(target: Desmos.ExpressionState)
12
12
  {
13
13
  super.require_expr_type(target.type, "expression");
14
- // @ts-expect-error: outdated types
15
14
  target.playing = true;
16
15
  }
17
16
  }
@@ -1,13 +1,14 @@
1
1
  import { Incantation, ArgIncantation, type LOCAL } from "../incantation";
2
- import { UnrecoverableError } from "../../errors";
3
2
 
4
3
  import { DesmosColour } from "../../desmos";
4
+ import { UnrecoverableError } from "../../errors";
5
+ import type { DesmostOptions } from "../../options";
5
6
 
6
7
 
7
8
  export class ColourIncantation extends ArgIncantation<LOCAL>
8
9
  {
9
10
  override readonly description
10
- = ""
11
+ = "Change the colour of a rendered expression, such as a line, region, polygon, etc."
11
12
 
12
13
  override readonly identifier = "color"
13
14
  override readonly alias = "colour"
@@ -20,7 +21,7 @@ export class ColourIncantation extends ArgIncantation<LOCAL>
20
21
  target.color = data;
21
22
  }
22
23
 
23
- override evaluate_arg(data: string): DesmosColour
24
+ override evaluate_arg(data: string, options: DesmostOptions): DesmosColour
24
25
  {
25
26
  switch (data.trim().toUpperCase()) {
26
27
  case "RED": return DesmosColour.RED;