diff --git a/.vitepress/config.js b/.vitepress/config.js index 7b39a57aef..5a58472f66 100644 --- a/.vitepress/config.js +++ b/.vitepress/config.js @@ -32,6 +32,7 @@ const config = defineConfig({ markdown: { languages, + math: true, toc: { level: [2,3] }, diff --git a/assets/cxl/binary-operator.drawio.svg b/assets/cxl/binary-operator.drawio.svg new file mode 100644 index 0000000000..5fcfd4a1c0 --- /dev/null +++ b/assets/cxl/binary-operator.drawio.svg @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + * + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + / + + + + + + + + + + = + + + + + + + + + + != + + + + + + + + + + == + + + + + + + + + + <> + + + + + + + + + + > + + + + + + + + + + >= + + + + + + + + + + < + + + + + + + + + + <= + + + + + + + + + + AND + + + + + + + + + + OR + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ || +
+
+
+
+ + || + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/assets/cxl/binding-parameter.drawio.svg b/assets/cxl/binding-parameter.drawio.svg new file mode 100644 index 0000000000..cdb44e74bd --- /dev/null +++ b/assets/cxl/binding-parameter.drawio.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + + + + + + + + + + : + + + + + + + + + + + + + + string-literal + + + + + + + + + + numeric-literal + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/cxl/expr.drawio.svg b/assets/cxl/expr.drawio.svg new file mode 100644 index 0000000000..3ddf4a2d1e --- /dev/null +++ b/assets/cxl/expr.drawio.svg @@ -0,0 +1,890 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + literal value + + + + + + + + + + + + ref + + + + + + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + + + + + + + + + + + expr + + + + + + + + + + + + expr + + + + + + + + + + + + + + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + + ( + + + + + + + + + + , + + + + + + + + + + ) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ( + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + + as + + + + + + + + + + + + + + + type-ref + + + + + + + + + + + ) + + + + + + + + + + + + + + + + + + + + + + + expr + + + + + + + + + + + CAST + + + + + + + + + + + + + + + + + + + + + LIKE + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + + + + + + + expr + + + + + + + + + + + NULL + + + + + + + + + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + + NOT + + + + + + + + + + + + + + + + + + BETWEEN + + + + + + + + + + NOT + + + + + + + + + + + + + + IS + + + + + + + + + + + + + + NOT + + + + + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + + AND + + + + + + + + + + + expr + + + + + + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + NOT + + + + + + + + + + + + + + + + + + ( + + + + + + + + + + ) + + + + + + + + + + + expr + + + + + + + + + + + + + + + , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NOT + + + + + + + + + + EXISTS + + + + + + + + + + + + + + + + + + IN + + + + + + + + + + + + + + CASE + + + + + + + + + + + + + + WHEN + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + + THEN + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + + + + + + ELSE + + + + + + + + + + + + + + + + + + + expr + + + + + + + + + + + END + + + + + + + + + + + + + + + + + + + + + + + unary operator + + + + + + + + + + + + + + + + binary operator + + + + + + + + + + + + binding-parameter + + + + + + + + + + + + + + + + + + + + ref + + + + + + + + + + + + + + + + + + + + function + + + + + + + + + + \ No newline at end of file diff --git a/assets/cxl/function-args.drawio.svg b/assets/cxl/function-args.drawio.svg new file mode 100644 index 0000000000..3147fcb304 --- /dev/null +++ b/assets/cxl/function-args.drawio.svg @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + expr + + + + + + + + + + + , + + + + + + + + + + + + + + + + + + + ordering-term + + + + + + + + + + + + + + + ORDER + + + + + + + + + + + + + + BY + + + + + + + + + + + + + + , + + + + + + + + + + + + + + + + + + + + + + param name + + + + + + + + + + => + + + + + + + + + + + expr + + + + + + + + + + + + + + + + + + + + + + + , + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/cxl/function-def.drawio.svg b/assets/cxl/function-def.drawio.svg new file mode 100644 index 0000000000..e1379c1424 --- /dev/null +++ b/assets/cxl/function-def.drawio.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + function name + + + + + + + + + + + + + + ( + + + + + + + + + + + + + + + + + + ) + + + + + + + + + + + expr + + + + + + + + + + + , + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/cxl/infix-filter-full.drawio.svg b/assets/cxl/infix-filter-full.drawio.svg new file mode 100644 index 0000000000..77c80df99f --- /dev/null +++ b/assets/cxl/infix-filter-full.drawio.svg @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + WHERE + + + + + + + + + + + + + [ + + + + + + + + + + + + + + ] + + + + + + + + + + + + + + GROUP BY + + + + + + + + + + + + + + LIMIT + + + + + + + + + + + ordering-term + + + + + + + + + + + ORDER BY + + + + + + + + + + , + + + + + + + + + + + + + + + + + + + expr + + + + + + + + + + + + expr + + + + + + + + + + + + + + + HAVING + + + + + + + + + + + + + + + expr + + + + + + + + + + + , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OFFSET + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/cxl/infix-filter.drawio.svg b/assets/cxl/infix-filter.drawio.svg new file mode 100644 index 0000000000..c46cbea71f --- /dev/null +++ b/assets/cxl/infix-filter.drawio.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + WHERE + + + + + + + + + + + + + + + + + [ + + + + + + + + + + + + + + ] + + + + + + + + + + + + + + + expr + + + + + + + + + + + + + + + + + + + +
+
+
+ WHERE is implicitly part of each infix filter +
+
+
+
+ + WHERE is implic... + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/assets/cxl/intro.drawio.svg b/assets/cxl/intro.drawio.svg new file mode 100644 index 0000000000..8383fa431e --- /dev/null +++ b/assets/cxl/intro.drawio.svg @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + Some shapes have tooltips with some explanation, try it out! 🚀 + + + + + + + Some shapes have tooltips with some explanation, try it out! 🚀 + + + alternative 1 + + + + + + + + + + alternative 2 + + + + + + + + + + + language construct + + + + + + + + + + + + + + + + start here 🚀 + + + + + + + + + + + + + + + + + + + 💡clickable + + + + + + + + + + + + + + + finish 🏁 + + + + + + + + + + + + + + + + + … + + + + + + + + + + + + + + Green Boxes indicate proprietary CAP expressions + + + they extend standard SQL Expressions in some way + + + + + + + + + + +
+
+
+ + Round shapes refer to terminal symbols such as + + + exists + + + or + + + null + + + , which can't be further divided + +
+
+
+
+ + Round shapes refer to te... + +
+
+
+ + + + + + + +
+
+
+ + language constructs can be broken down further and get their own syntax diagram + +
+
+
+
+ + language constructs can... + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/assets/cxl/literal-value.drawio.svg b/assets/cxl/literal-value.drawio.svg new file mode 100644 index 0000000000..fc903bfbd9 --- /dev/null +++ b/assets/cxl/literal-value.drawio.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + string-literal + + + + + + + + + + NULL + + + + + + + + + + + + + + + + + + numeric-literal + + + + + + + + + + + + + + + + + + TRUE + + + + + + + + + + FALSE + + + + + + + + + + + + + + + + + + + + + + + + + + Date + + + + + + + + + + Time + + + + + + + + + + DateTimes + + + + + + + + + + Array + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/cxl/ordering-term.drawio.svg b/assets/cxl/ordering-term.drawio.svg new file mode 100644 index 0000000000..b349dda9fd --- /dev/null +++ b/assets/cxl/ordering-term.drawio.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + expr + + + + + + + + + + + ASC + + + + + + + + + + DESC + + + + + + + + + + NULLS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NULLS + + + + + + + + + + + + + + FIRST + + + + + + + + + + LAST + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/cxl/over-clause.drawio.svg b/assets/cxl/over-clause.drawio.svg new file mode 100644 index 0000000000..6c89b7f33a --- /dev/null +++ b/assets/cxl/over-clause.drawio.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + OVER + + + + + \ No newline at end of file diff --git a/assets/cxl/ref.drawio.svg b/assets/cxl/ref.drawio.svg new file mode 100644 index 0000000000..f479b81283 --- /dev/null +++ b/assets/cxl/ref.drawio.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + a scalar element is a primitive field (e.g., String, Integer, Decimal, Boolean, Date/Time, UUID) without nested structure or associations + + + + + + + a scalar element is a primitive field (e.g., String, Integer, Decimal, Boolean, Date/Time, UUID) without nested structure or associations + + + leaf element + + + + + + + + + + structured element + + + + + + + + + + association + + + + + + + + + + + infix filter + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ . +
+
+
+
+ + . + +
+
+
+ + + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/assets/cxl/sets-expand.drawio.svg b/assets/cxl/sets-expand.drawio.svg new file mode 100644 index 0000000000..501f38ef5b --- /dev/null +++ b/assets/cxl/sets-expand.drawio.svg @@ -0,0 +1,366 @@ + + + + + + + + + + +
+
+
+
+
+ + select from + + + + + + Authors + + + + { + +
+
+ + name, + +
+
+ + books[ + + + + stock > 100 + + + + ] { title } + +
+
+ + } + +
+
+
+
+ + { name: + + + + + 'Emily Brontë', + + + + books: [] + + + + + }, + + +
+ + { name: + + + + + + 'Charlotte Brontë', + + + + books: [] + + + + + }, + +
+
+ + + { + + +
+
+ + + + name: + + + + + 'Charlotte Brontë', + + + + + + books: [] + + + + }, + + + +
+
+ + + books: [ + + +
+
+ + + { + + + + title: + + + + 'The Raven', + + + + stock: + + + + 333 + + + + + + + }, + + +
+
+ + + { + + + + + + title: + + + + 'Eleonora', + + + + stock: + + + + 555 + + + + + + + } + + +
+
+ + + ] + + +
+
+ + + } + + +
+
+
+ + + { name: + + + + 'Richard Carpenter', + + + + + books: [] + + + + + } + + + +
+
+
+
+
+ + select from Authors {... + +
+
+
+ + + + + + + + + + + + + +
+
+
+ + Authors + +
+
+
+
+ + Authors + +
+
+
+ + + + + + + +
+
+
+ + Books + +
+ [ + + stock > 100 + + ] +
+
+
+
+
+ + Books... + +
+
+
+ + + + + + + +
+
+
+ no matching + + books + +
+
+
+
+ + no matching boo... + +
+
+
+ + + + + + + +
+
+
+ matching +
+ + books + +
+
+
+
+
+ + matching... + +
+
+
+ + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/assets/cxl/sets-intersection.drawio.svg b/assets/cxl/sets-intersection.drawio.svg new file mode 100644 index 0000000000..24df038882 --- /dev/null +++ b/assets/cxl/sets-intersection.drawio.svg @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + +
+
+
+ + Books + +
+
+
+
+ + Books + +
+
+
+ + + + + + + +
+
+
+ + stock > 100 + +
+
+
+
+ + stock > 100 + +
+
+
+ + + + + + + + + + +
+
+
+ + + Books + + +
+ + [ + + + + stock > 100 + + + + ] + +
+
+
+
+
+ + Books... + +
+
+
+ + + + + + + +
+
+
+ + Authors + +
+
+
+
+ + Authors + +
+
+
+ + + + + + + + + + + +
+
+
+ + select from + + + Authors + + { name } +
+ + where exists + + books[ + + stock > 100 + + ] +
+
+
+
+
+ + select from Authors { name }... + +
+
+
+ + + + + + + + + + +
+
+
+ [ { name: + + 'Edgar Allen Poe' + + } ] +
+
+
+
+ + [ { name: 'Edgar Allen Poe' } ] + +
+
+
+ + + + + + + + + + + +
+
+
+
+ + select from + + + + + + Books + + + + [ + + + + stock > 100 + + + + ] + + + + { + + + + title + + + + } + + +
+
+
+
+ { title: + + 'The Raven' + + }, +
+ { title: + + 'Eleonora' + + } +
+
+
+
+
+ + select from Books[ stock > 100 ] { title }... + +
+
+
+ + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/assets/cxl/sets-leftjoin.drawio.svg b/assets/cxl/sets-leftjoin.drawio.svg new file mode 100644 index 0000000000..1f37197e08 --- /dev/null +++ b/assets/cxl/sets-leftjoin.drawio.svg @@ -0,0 +1,373 @@ + + + + + + + + + + + + + + + + +
+
+
+ + Authors + +
+ [ + + age > 40 + + ] +
+
+
+
+
+ + Authors... + +
+
+
+ + + + + + + +
+
+
+ + Books + +
+
+
+
+ + Books + +
+
+
+ + + + + + + +
+
+
+ + + + + select from + + + + + + Books + + + { +
+
+
+
+
+ + + + + title, + + + + +
+
+ + + + author[ + + + + + age > 40 + + + + + ].name as author + + + +
+
+ + + + } + + + +
+
+ + + +
+
+
+
+
+
+ + + + + { title: + + + + + + 'Wuthering Heights', + + + + + + author: null + + + + + + }, + + + + + + +
+
+ + + + + + { title: + + + + + + + 'Jane Eyre', + + + + + author: null + + + + + + + }, + + + + + + + + +
+
+ + + + + + { title: + + + + + + 'The Raven', + + + + + + + author: null + + + + + + }, + + + + + + + +
+
+ + + + + + { title: + + + + + + 'Eleonora' + + + + + + } + + + + + +
+
+ + + + + { title: + + + + + + 'Catweazle', + + + author: null + + + + + + } + + + + +
+
+
+
+
+ + select from Books {... + +
+
+
+ + + + + + + +
+
+
+ no matching + + author + +
+
+
+
+ + no matching aut... + +
+
+
+ + + + + + + +
+
+
+ matching +
+ + author + +
+
+
+
+
+ + matching... + +
+
+
+ + + + + + + + + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/assets/cxl/unary-operator.drawio.svg b/assets/cxl/unary-operator.drawio.svg new file mode 100644 index 0000000000..9158f4579c --- /dev/null +++ b/assets/cxl/unary-operator.drawio.svg @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + NOT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cds/cql-draft.md b/cds/cql-draft.md new file mode 100644 index 0000000000..7348a2f24d --- /dev/null +++ b/cds/cql-draft.md @@ -0,0 +1,92 @@ +--- +# layout: cds-ref +shorty: Query Language +synopsis: > + Specification of the CDS Query Language (CXL) used to capture expressions in CDS. +status: draft +--- + + + +# CDS Query Language (CQL) { #ql } + +::: info this is a draft and contains some samples which were taken from the CXL docs +::: + +# Sample Collection + +## function args { #function-args } + +
+ +
+
+ +### aggregate function with ordering term + +::: code-group +```js [CAP Style] {4} +> await cds.ql` + SELECT from Authors { + name, + string_agg(books.title, ', ' ORDER BY books.title DESC) as titles + } + GROUP BY books.author.ID` + +[ + { name: 'Emily Brontë', titles: 'Wuthering Heights' }, + { name: 'Charlotte Brontë', titles: 'Jane Eyre' }, + { name: 'Edgar Allen Poe', titles: 'The Raven, Eleonora' }, + { name: 'Richard Carpenter', titles: 'Catweazle' } +] +``` + +```js [SQL Style] +await cds.ql` + SELECT + name, + string_agg(books.title, ', ' ORDER BY books.title DESC) as titles + from Authors as A + left join Books as books on books.author_ID = A.ID + GROUP BY books.author_ID +``` + +```sql [SQL output] +SELECT + "$A".name, + string_agg(books.title, ? ORDER BY books.title DESC) AS titles +FROM sap_capire_bookshop_Authors AS "$A" +LEFT JOIN sap_capire_bookshop_Books AS books + ON books.author_ID = "$A".ID +GROUP BY books.author_ID; +``` +::: + + + + + diff --git a/cds/cxl.md b/cds/cxl.md new file mode 100644 index 0000000000..33b519ca2b --- /dev/null +++ b/cds/cxl.md @@ -0,0 +1,951 @@ +--- +# layout: cds-ref +shorty: Expressions +synopsis: > + Specification of the CDS Expression Language (CXL) used to capture expressions in CDS. +status: draft +--- + + + + +::: danger This documentation is a work in progress and will change over time. +::: + +# CDS Expression Language (CXL) { #expressions } +The CDS Expression Language (`CXL`) is a language to express calculations, conditions, +and other expressions in the context of CDS models and queries. +**`CXL` is based on the SQL expression language**, so many syntax elements from SQL are also available in `CXL`. + +`CXL` can be used in various places: +- In [CQL](./cql#path-expressions) (select list, where clause, …) +- In [CDL](./cdl) + + In [calculated elements](./cdl/#calculated-elements) + + In [annotations](./cdl.md#expressions-as-annotation-values) + +::: info 💡 Expressions in CAP are materialized in the context of queries +No matter where `CXL` is used, it always manifests in queries. +For example, [a calculated element](./cdl/#calculated-elements) defined in an entity will be resolved +to the respective calculation in the generated query when the entity is queried. +::: + + +## How to read this guide { #how-to } + + +In the following chapters we illustrate the `CXL` syntax based on simple and more complex examples. +For a complete reference of the syntax, there are clickable [syntax diagrams](https://en.wikipedia.org/wiki/Syntax_diagram) (aka railroad diagrams) for each language construct. + +### samples + +To try the samples by yourself, create a simple CAP app: + +```sh +cds init bookshop --add sample && cd bookshop +``` + +We encourage you to play around with the snippets. +Just create the sample app as described above and start a repl session within the newly created app by running: + +```sh +cds repl --run . +``` + +:::info 💡 All of the example expressions follow the same pattern: +1. A **`CXL`** is shown in the context of a query. +2. The resulting **`SQL`** is shown. + +:::code-group +```js [CQL] +> await cds.ql`SELECT from Books { title }` // [!code focus] +[ + { title: 'Wuthering Heights' }, + { title: 'Jane Eyre' }, + { title: 'The Raven' }, + { title: 'Eleonora' }, + { title: 'Catweazle' } +] +``` + +```sql [SQL] +SELECT Books.title FROM sap_capire_bookshop_Books as Books +``` +::: + +### syntax diagrams + +Each language construct is illustrated by a clickable [syntax diagram](https://en.wikipedia.org/wiki/Syntax_diagram). + +They show the syntax of CAPs expression language as a sequence of building blocks. +By clicking on the individual blocks, you can get more information about the respective building block. + +The following diagram illustrates how to read the diagrams: + +
+ +### theoretical background + +CAP did not re-invent when it comes to expressions. +It rather builds upon well-known concepts from relational databases and SQL. + +In the [final chapter](#foundation) of this guide, we provide some theoretical background. + +## expr { #expr } + +An expression can hold various elements, such as references, literals, function calls, operators, and more. A few examples, in the context of a select list: +```cds +select from Books { + 42 as answer, // literal + title, // reference ("ref") + price * quantity as totalPrice, // binary operator + substring(title, 1, 3) as shortTitle, // function call + author.name as authorName, // ref with path expression + chapters[number < 3] as earlyChapters, // ref with infix filter + exists chapters as hasChapters, // exists + count(chapters) as chapterCount, // aggregate function +} +``` + + +
+ +
+
+ +TODO: Some samples --> Where can we use expressions? + +- annotation expression +- calculated element +- one in select + +## ref (path expression) { #ref } + +A `ref` (short for reference) is used to refer to an element within the model. +It can be used to navigate along path segments. Such a navigation is often +referred to as a **path expression**. + +
+ +
+
+ +::: info 💡 Leaf elements +Leaf elements as opposed to associations and structured elements represent scalar values, such as strings, numbers, dates, as well as the array and map types. +They typically manifest as columns in database tables. +::: + +### simple element reference + +In its simplest form, a `ref` can be used to reference an element: + +:::code-group +```js [CQL] {1} +> await cds.ql`SELECT from Books { title }` // [!code focus] +[ + { title: 'Wuthering Heights' }, + { title: 'Jane Eyre' }, + { title: 'The Raven' }, + { title: 'Eleonora' }, + { title: 'Catweazle' } +] +``` + +```sql [SQL] +SELECT Books.title FROM sap_capire_bookshop_Books as Books +``` +::: + +In this example, we select the `title` element from the `Books` entity. + +### path navigation {#path-navigation} + +A path expression can be used to navigate to any element of the associations target: + +:::code-group +```js [CQL] +> await cds.ql`SELECT from Books { title, author.name as author }` // [!code focus] +[ + { title: 'Wuthering Heights', author: 'Emily Brontë' }, + { title: 'Jane Eyre', author: 'Charlotte Brontë' }, + { title: 'The Raven', author: 'Edgar Allen Poe' }, + { title: 'Eleonora', author: 'Edgar Allen Poe' }, + { title: 'Catweazle', author: 'Richard Carpenter' } +] +``` + +```sql [SQL] +SELECT + Books.title, + author.name AS author +FROM + sap_capire_bookshop_Books AS Books + LEFT JOIN sap_capire_bookshop_Authors AS author -- The table alias for association 'author' + ON author.ID = Books.author_ID; +``` +::: + +In this example, we select all books together with the name of their author. +`author` is an association in the `Books` entity. + +::: info 💡 Associations are **forward-declared joins** +They provide a convenient way to navigate between related entities without having to define the join conditions manually. + +The join condition is defined **ahead of time** as part of the association. +Typically, this is a foreign key relationship between two entities, but other conditions are also possible. + +It then manifests whenever the association is used in a path expression as part of a query. + +The condition can manifest in multiple ways: +- In the on condition of a join +- In the condition that correlates a subquery to a main query +- To select related entities with an additional query +::: + +### in the from clause {#in-from-clause} + +A path expression can also be used in the `from` clause of a query to navigate to a related entity: + +:::code-group +```js [CQL] +> await cds.ql`SELECT from Books:author { name }` // [!code focus] +[ + { name: 'Emily Brontë' }, + { name: 'Charlotte Brontë' }, + { name: 'Edgar Allen Poe' }, + { name: 'Richard Carpenter' } +] +``` + +```sql [SQL] +SELECT Authors.name +FROM sap_capire_bookshop_Authors as Authors +WHERE exists ( + SELECT 1 + FROM sap_capire_bookshop_Books as Books + WHERE Books.author_ID = Authors.ID +) +``` +::: + +TODO explanation + +This is equivalent to writing `SELECT from Authors where exists books`. When combining this with [infix filters](#infix-filter), it allows for quite concise queries. + +### in the where clause {#in-where-clause} + +A path expression can also be used as part of the where clause to filter based on elements of related entities: + +:::code-group +```js [CQL] +> await cds.ql`SELECT from Books { title } where genre.name = 'Fantasy'` // [!code focus] +[ { title: 'Catweazle' } ] +``` + +```sql [SQL] +SELECT + Books.title +FROM + sap_capire_bookshop_Books AS Books + LEFT JOIN sap_capire_bookshop_Genres AS genre + ON genre.ID = Books.genre_ID +WHERE + genre.name = ? +``` +::: + +In this example, we select all books that belong to the `Fantasy` genre. +The table alias for the `genre` association is used in the where clause of the SQL query. + +### in order by + +A path expression can also be used in the `order by` clause to sort based on elements of related entities: + +:::code-group +```js [CQL] {4} +> await cds.ql` + SELECT from Books + { title, author.dateOfBirth as birthDate } + order by author.dateOfBirth` +[ + { title: 'The Raven', birthDate: '1809-01-19' }, + { title: 'Eleonora', birthDate: '1809-01-19' }, + { title: 'Jane Eyre', birthDate: '1818-04-21' }, + { title: 'Wuthering Heights', birthDate: '1818-07-30' }, + { title: 'Catweazle', birthDate: '1929-08-14' } +] +``` + +```sql [SQL] +SELECT + Books.title, + author.dateOfBirth AS birthDate +FROM + sap_capire_bookshop_Books AS Books + LEFT JOIN sap_capire_bookshop_Authors AS author + ON author.ID = Books.author_ID +ORDER BY + author.dateOfBirth ASC +``` +::: + +In this example, we select all books and order them by the date of birth of their authors. +The table alias for the `author` association is used in the order by clause of the SQL query. + +### after `exists` predicate + +path expressions can also be used after the `exists` predicate to check for the existence. +This is especially useful for to-many relations. + +E.g., to select all authors that have written **at least** one book: + +:::code-group +```js [CQL] {1} +> await cds.ql`SELECT from Authors { name } where exists books` + +[ + { name: 'Emily Brontë' }, + { name: 'Charlotte Brontë' }, + { name: 'Edgar Allen Poe' }, + { name: 'Richard Carpenter' } +] +``` + +```sql [SQL] {3-7} +SELECT Authors.name +FROM sap_capire_bookshop_Authors as Authors +WHERE exists ( + SELECT 1 + FROM sap_capire_bookshop_Books as books + WHERE books.author_ID = Authors.ID + ) +``` +::: + +::: info 💡 Learn more about the `exists` predicate [here](./cql.md#exists-predicate) +::: + +## infix filter { #infix-filter } + +An infix in linguistics refer to a letter or group of letters that are added in the middle of a word to make a new word. + +If we apply this terminology to [path-expressions](#ref), an infix filter condition is an expression +that is applied to a path-segment of a [path-expression](#ref). +This allows to filter the target of an association based on certain criteria. + +
+ +
+
+ + +::: info 💡 infix notation as a way to influence auto-generated subqueries +Within an infix, more than than just a simple `WHERE` condition can be specified. +It is also possible to use other query modifiers, such as `GROUP BY`, `HAVING`, `ORDER BY`, `LIMIT`, and `OFFSET`. + +::: details see the full syntax diagram + +TODO: make diagram more readable (use vertical dimension) + +
+ +
+
+ + ::: warning + Query modifiers other than `WHERE` may only be used in the context of nested projections + and `exists` predicates. They are ignored for regular path expressions. + + +::: + +### applied to `exists` predicate + +In this example, we want to select all authors that have written at least one book in the `Fantasy` genre: + +> REVISIT: this does work in the node runtime, but the compiler does not yet support it. How about java? + +:::code-group +```js [CQL] +> await cds.ql` + SELECT from Authors { name } + where exists books[genre.name = 'Fantasy']` // [!code focus] + +[ { name: 'Richard Carpenter' } ] +``` + +```sql [SQL] +SELECT Authors.name +FROM sap_capire_bookshop_Authors as Authors +WHERE exists ( + SELECT 1 + FROM sap_capire_bookshop_Books as books + inner JOIN sap_capire_bookshop_Genres as genre + ON genre.ID = books.genre_ID + WHERE books.author_ID = Authors.ID + and genre.name = 'Fantasy' + ) +``` +::: + +> Note how the infix filter condition `genre.name = 'Fantasy'` is applied to the +subquery following the `exists` predicate for the `books` association. + +### applied to `expand` + +Further narrow down the result set of a path expression by applying an infix filter condition to +[nested-expands](./cql#nested-expands): + +::: code-group +```js [CQL] {3} +> await cds.ql` + SELECT from Authors { name, + books[ price < 19.99 ] as cheapBooks { + title, + price + } + }` + +[ + { + name: 'Emily Brontë', + cheapBooks: [ { title: 'Wuthering Heights', price: 11.11 } ] + }, + { + name: 'Charlotte Brontë', + cheapBooks: [ { title: 'Jane Eyre', price: 12.34 } ] + }, + { + name: 'Edgar Allen Poe', + cheapBooks: [ + { title: 'The Raven', price: 13.13 }, + { title: 'Eleonora', price: 14 } + ] + }, + { name: 'Richard Carpenter', cheapBooks: [] } +] +``` + +```sql [SQL] +SELECT Authors.name, + ( + SELECT jsonb_group_array(jsonb_insert('{}', '$."title"', title)) as _json_ + FROM ( + SELECT cheapBooks.title + FROM sap_capire_bookshop_Books as cheapBooks + WHERE Authors.ID = cheapBooks.author_ID + and cheapBooks.price < 19.99 + ) + ) as cheapBooks +FROM sap_capire_bookshop_Authors as Authors +``` +::: + +
+ +::: info 💡 JSON functions + +In this example, the runtime makes use of JSON functions to aggregate the related `books` into a JSON array. +This is because SQL databases do not have a native concept of nested result sets. + +> TODO: Link to guide about JSON functions, What about java? +::: + +
+ + +### with query modifiers + +It is also possible to influence the generated subquery, +by adding other query modifiers, such as `GROUP BY`, `HAVING`, `ORDER BY`, `LIMIT`, and `OFFSET`: + +::: code-group +```js [CQL] {3} +> await cds.ql` + SELECT from Authors { name, + books[ price < 19.99 order by title desc ] as cheapBooks { + title, + price + } + }` + +[ + { + name: 'Emily Brontë', + cheapBooks: [ { title: 'Wuthering Heights', price: 11.11 } ] + }, + { + name: 'Charlotte Brontë', + cheapBooks: [ { title: 'Jane Eyre', price: 12.34 } ] + }, + { + name: 'Edgar Allen Poe', + cheapBooks: [ + { title: 'The Raven', price: 13.13 }, + { title: 'Eleonora', price: 14 } + ] + }, + { name: 'Richard Carpenter', cheapBooks: [] } +] +``` + +```sql [SQL] +SELECT Authors.name, + ( + SELECT jsonb_group_array(jsonb_insert('{}', '$."title"', title)) as _json_ + FROM ( + SELECT cheapBooks.title + FROM sap_capire_bookshop_Books as cheapBooks + WHERE Authors.ID = cheapBooks.author_ID + and cheapBooks.price < 19.99 + ORDER BY title DESC -- ORDER BY added to subquery + ) + ) as cheapBooks +FROM sap_capire_bookshop_Authors as Authors +``` +::: + + +TODO: add some short explanation and the limitations + +### applied to `from` clause + + + +:::code-group +```js [CQL] +> await cds.ql`SELECT from Books[price > 19.99]:author { name }` // [!code focus] +[ { name: 'Richard Carpenter' } ] +``` + +```sql [SQL] +SELECT Authors.name +FROM sap_capire_bookshop_Authors as Authors +WHERE exists ( + SELECT 1 + FROM sap_capire_bookshop_Books as Books + WHERE Books.author_ID = Authors.ID + and Books.price > ? +) +``` +::: + +TODO: explanation and intro to the next sample with [query modifiers](#with-query-modifiers) + + +:::code-group + +```js [CQL] {3,4,5} +await cds.ql` + SELECT from Books[ + where stock > 0 + group by genre + order by genre asc + ] + { + avg(price) as avgPrice, + genre.name + } +` +[ + { avgPrice: 11.725, genre_name: 'Drama' }, + { avgPrice: 150, genre_name: 'Fantasy' }, + { avgPrice: 14, genre_name: 'Romance' }, + { avgPrice: 13.13, genre_name: 'Mystery' } +] + +``` + +```sql [SQL] {6,7,8} +SELECT + avg(Books.price) as avgPrice, + genre.name as genre_name +FROM sap_capire_bookshop_Books as Books + left JOIN sap_capire_bookshop_Genres as genre ON genre.ID = Books.genre_ID +WHERE Books.stock > 0 +GROUP BY Books.genre_ID +ORDER BY Books.genre_ID ASC +``` +::: + +::: details The above is equivalent to… + +Using the infix notation to specify the query modifiers is just +syntactic sugar: + +```js +await cds.ql` + SELECT from Books + { + avg(price) as avgPrice, + genre.name + } + where stock > 0 + group by genre + order by genre asc` +``` + +::: + +### in calculated element + +You can also use the infix filter notation to derive +another more specific association from an existing one. + +In the `Authors` entity in the `Books.cds` file add a new element `cheapBooks`: + +```cds {2} + books : Association to many Books on books.author = $self; + cheapBooks = books[price < 19.99]; // based on `books` association +``` + +Now we can use `cheapBooks` just like any other association. +E.g. to select the set of authors which have no cheap books: + +:::code-group +```js [CQL] {1} +> await cds.ql`SELECT from Authors { name } where not exists cheapBooks` +[ + { name: 'Richard Carpenter' } +] +``` + +```sql [SQL] +SELECT Authors.name +FROM sap_capire_bookshop_Authors as Authors +WHERE not exists ( + SELECT 1 + FROM sap_capire_bookshop_Books as cheapBooks + WHERE (cheapBooks.author_ID = Authors.ID) + and (cheapBooks.price < 19.99) -- here the infix filter condition is applied + ) +``` +::: + + +::: info 💡 Learn more about association-like calculated elements [here](./cdl.md#association-like-calculated-elements). +::: + +We can also use `cheapBooks` in nested expands to get all cheap books of each author: + +::: code-group +```js [CQL] +> await cds.ql`SELECT from Authors { name, cheapBooks { title, price } }` // [!code focus] +[ + { + name: 'Emily Brontë', + cheapBooks: [ { title: 'Wuthering Heights', price: 11.11 } ] + }, + { + name: 'Charlotte Brontë', + cheapBooks: [ { title: 'Jane Eyre', price: 12.34 } ] + }, + { + name: 'Edgar Allen Poe', + cheapBooks: [ + { title: 'The Raven', price: 13.13 }, + { title: 'Eleonora', price: 14 } + ] + }, + { name: 'Richard Carpenter', cheapBooks: [] } +] +``` + +```sql [SQL] +SELECT Authors.name, +( + SELECT jsonb_group_array( + jsonb_insert('{}', '$."title"', title, '$."price"', price) + ) as _json_ + FROM ( + SELECT + Books.title, + Books.price + FROM sap_capire_bookshop_Books as Books + WHERE (Authors.ID = Books.author_ID) + and (Books.price < ?) + ) +) as cheapBooks +FROM sap_capire_bookshop_Authors as Authors +``` +::: + + +> TODO: explanation about json functions again? Learn more link? Specific additions? + + +### between path segments + +TODO: simpler example a la `SELECT from Books { title, author[age < 40].name }` + +In this case we want to select all books where the author's name starts with `Emily` +and the author is younger than 40 years. + +:::code-group +```js [CQL] {4} +> await cds.ql` + SELECT from Books { title } + where startswith( + author[ years_between(dateOfBirth, dateOfDeath) < 40 ].name, + 'Emily' + )` + +[ { title: 'Wuthering Heights' } ] +``` + +```sql [SQL] +SELECT + title +FROM + sap_capire_bookshop_Books AS B + LEFT JOIN sap_capire_bookshop_Authors AS author + ON B.author_ID = author.ID + AND FLOOR( + ( + ( + (CAST(STRFTIME('%Y', author.dateOfDeath) AS INTEGER) - CAST(STRFTIME('%Y', author.dateOfBirth) AS INTEGER)) * 12 + ) + ( + CAST(STRFTIME('%m', author.dateOfDeath) AS INTEGER) - CAST(STRFTIME('%m', author.dateOfBirth) AS INTEGER) + ) + ( + CASE + WHEN (CAST(STRFTIME('%Y%m', author.dateOfDeath) AS INTEGER) < CAST(STRFTIME('%Y%m', author.dateOfBirth) AS INTEGER)) THEN + (CAST(STRFTIME('%d%H%M%S%f0000', author.dateOfDeath) AS INTEGER) > CAST(STRFTIME('%d%H%M%S%f0000', author.dateOfBirth) AS INTEGER)) + ELSE + (CAST(STRFTIME('%d%H%M%S%f0000', author.dateOfDeath) AS INTEGER) < CAST(STRFTIME('%d%H%M%S%f0000', author.dateOfBirth) AS INTEGER)) * -1 + END + ) + ) / 12 + ) < ? +WHERE + COALESCE(INSTR(author.name, ?) = 1, FALSE); +``` +::: + +The path expression `author[ years_between(dateOfBirth, dateOfDeath) < 40 ].name` +navigates along the `author` association of the `Books` entity. + +The join for this path expression is generated as usual and enhanced with the infix filter condition `years_between(dateOfBirth, dateOfDeath) < 40`. + + +::: info 💡 Standard functions +the `years_between` and `startswith` functions are in the [set of CAPs standard functions](../guides/databases.md#standard-database-functions) and are translated to the respective SQL to get the desired result. +::: + + + +## operators + +### unary operator { #unary-operator } + +
+
+
+ + +::: info 💡 A unary operator is an operator that operates on exactly one operand. + +E.g. in the expression `-price`, the `-` operator is a unary operator +that operates on the single operand `price`. It negates the value of `price`. +::: + +### binary operator { #binary-operator } + +
+
+
+ + +::: info 💡 A binary operator is an operator that operates on two operands. +E.g. in the expression `price * quantity`, the `*` operator is a binary operator +that multiplies the two factors `price` and `quantity`. +::: + +## literal value { #literal-value } + +
+
+
+ +::: info 💡 Learn more about literals [here](./csn.md#literals) +::: + +## binding parameter { #binding-parameter } + +
+ +TODO: Remove for first version? + +💡 string and numeric literal as well as `?` are parsed as `ref` + +## function { #function } + + +
+ +
+
+ + + +CAP supports a set of [standard functions](../guides/databases.md#standard-database-functions) that can be used in expressions. In addition, functions are passed through to the underlying database, allowing you to leverage database-specific functions as needed. + +CAP standard functions: +| Name | Description | +|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| **String Functions** | | +| `concat(x, y, ...)` | Concatenates the given strings or numbers `x`, `y`, ... | +| `trim(x)` | Removes leading and trailing whitespaces from `x`. | +| `contains(x, y)` | Checks whether `x` contains `y` (case-sensitive). | +| `startswith(x, y)` | Checks whether `x` starts with `y` (case-sensitive). | +| `endswith(x, y)` | Checks whether `x` ends with `y` (case-sensitive). | +| `matchespattern(x, y)` | Checks whether `x` matches the regular expression `y`. | +| `indexof(x, y)`1 | Returns the index of the first occurrence of `y` in `x` (case-sensitive). | +| `substring(x, i, n?)`1 | Extracts a substring from `x` starting at index `i` (0-based) with an optional length `n`. | +| `length(x)` | Returns the length of the string `x`. | +| `tolower(x)` | Converts all characters in `x` to lowercase. | +| `toupper(x)` | Converts all characters in `x` to uppercase. | +| **Numeric Functions** | | +| `ceiling(x)` | Rounds the numeric parameter up to the nearest integer. | +| `floor(x)` | Rounds the numeric parameter down to the nearest integer. | +| `round(x)` | Rounds the numeric parameter to the nearest integer. The midpoint between two integers is rounded away from zero (e.g., `0.5` → `1` and `-0.5` → `-1`). | +| **Aggregate Functions** | | +| `min(x)` | Returns the minimum value of `x`. | +| `max(x)` | Returns the maximum value of `x`. | +| `sum(x)` | Returns the sum of all values of `x`. | +| `average(x)` | Returns the average (mean) value of `x`. | +| `count(x)` | Returns the count of non-null values of `x`. | +| `countdistinct(x)` | Returns the count of distinct non-null values of `x`. | + + + +## ordering term { #ordering-term } + +
+ +### ordered list of book titles by price + +:::code-group +```js [CQL] +> await cds.ql` + SELECT from Books { title, price } + order by price desc nulls last` // [!code focus] +[ + { title: 'Catweazle', price: 150 }, + { title: 'Eleonora', price: 14 }, + { title: 'The Raven', price: 13.13 }, + { title: 'Jane Eyre', price: 12.34 }, + { title: 'Wuthering Heights', price: 11.11 }, + { title: 'Untitled', price: null } +] +``` + +```sql [SQL] +SELECT + Books.title, + Books.price +FROM + sap_capire_bookshop_Books AS Books +ORDER BY price DESC NULLS LAST -- [!code focus] +``` +::: + +In this example, the ordering term sorts books by price in descending order and places rows with `null` prices at the end. + + +## type-ref { #type-ref } + +[Learn more about type references in CDL](./cdl#type-references){ .learn-more } + +## Scientific Background {#foundation} + +Every entity defines a set of all possible instances: +$${ b \in \text{Books} }$$ + +A simple select query on Books returns the complete set → all books. +Filters progressively narrow down the set: + +$$\text{highstock} = \{ b \in \text{Books} \mid b.\text{stock} > 100 \}$$ + +With the infix filter notation, we write it as `Books[stock > 100]`. +An association defines a relationship between two sets: + +$$\text{books} = \{ (a,b) \in \text{Books} \times \text{Authors} \mid b.\text{author\_id} = a.\text{id} \}$$ + +We can select this set using the path expression `Authors:books` in the [from clause](#in-from-clause). +The same can be applied to navigate via a path expression in the [select list](#path-navigation) or [where clause](#in-where-clause) using `books`. +Filtering authors by `Authors where exists books[stock > 100]` can be expressed as: + +$$\{ a \in \text{Authors} \mid \exists \space b \in \text{Books}( b.\text{author\_id} = a.\text{id} \land b.\text{stock} > 100 ) \}$$ + +Using the previously defined $\text{books}$, we can simplify it to: + +$$\{ a \in \text{Authors} \mid \exists \space b \in \text{books}( b.\text{stock} > 100 ) \}$$ + +Using the $\text{highstock}$ set, we can further simplify it to: + +$$\{ a \in \text{Authors} \mid \exists \space b \in \text{books} \cap \text{highstock} \}$$ + +So in conclusion, the expression filters for the intersection of the two sets $\text{books}$ (via association) and $\text{highstock}$ (via infix filter). + + + + + +
+
+
+ + + + + +
+
+
+ + +
+
+
+ + + + diff --git a/menu.md b/menu.md index 53a289d38e..81684c067b 100644 --- a/menu.md +++ b/menu.md @@ -127,6 +127,7 @@ ## [Query Language (CQL)](cds/cql) ## [Query Notation (CQN)](cds/cqn) ## [Expressions (CXN)](cds/cxn) +## [Expression Language (CXL)](cds/cxl) ## [Core / Built-in Types](cds/types) ## [Common Reuse Types](cds/common) ## [Common Annotations](cds/annotations) diff --git a/package-lock.json b/package-lock.json index 62c1fa3546..f6a8dc8bf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "eslint-plugin-vue": "^10.0.0", "fflate": "^0.8.2", "gray-matter": "^4.0.3", + "markdown-it-mathjax3": "^4.3.2", "markdownlint-cli": ">=0.35.0", "markdownlint-rule-search-replace": "^1.1.1", "sass": "^1.62.1", @@ -3304,6 +3305,16 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz", + "integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3402,6 +3413,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -3684,6 +3705,45 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.6.0.tgz", + "integrity": "sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "css-select": "^4.3.0", + "css-what": "^6.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.3.1", + "domutils": "^2.8.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -4044,6 +4104,36 @@ "node": ">=20" } }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4181,6 +4271,75 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4324,6 +4483,19 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -4508,6 +4680,16 @@ "node": "*" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -5186,6 +5368,36 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -5488,6 +5700,36 @@ "node": ">=0.10.0" } }, + "node_modules/juice": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-8.1.0.tgz", + "integrity": "sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio": "1.0.0-rc.10", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/juice/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/katex": { "version": "0.16.22", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", @@ -5628,6 +5870,17 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/markdown-it-mathjax3": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/markdown-it-mathjax3/-/markdown-it-mathjax3-4.3.2.tgz", + "integrity": "sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "juice": "^8.0.0", + "mathjax-full": "^3.2.0" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -5763,6 +6016,19 @@ "node": ">= 0.4" } }, + "node_modules/mathjax-full": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.1.tgz", + "integrity": "sha512-aUz9o16MGZdeiIBwZjAfUBTiJb7LRqzZEl1YOZ8zQMGYIyh1/nxRebxKxjDe9L+xcZCr2OHdzoFBMcd6VnLv9Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esm": "^3.2.25", + "mhchemparser": "^4.1.0", + "mj-context-menu": "^0.6.1", + "speech-rule-engine": "^4.0.6" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -6016,6 +6282,13 @@ "node": ">= 0.6" } }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", + "dev": true, + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -6038,6 +6311,13 @@ "node": ">= 0.6" } }, + "node_modules/mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -6705,6 +6985,13 @@ "dev": true, "license": "MIT" }, + "node_modules/mj-context-menu": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", + "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6764,6 +7051,27 @@ "license": "MIT", "optional": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -6907,6 +7215,23 @@ "dev": true, "license": "MIT" }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -7564,6 +7889,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "dev": true, + "license": "MIT (http://mootools.net/license.txt)", + "engines": { + "node": "*" + } + }, "node_modules/smol-toml": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.5.2.tgz", @@ -7608,6 +7943,31 @@ "node": ">=0.10.0" } }, + "node_modules/speech-rule-engine": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.1.2.tgz", + "integrity": "sha512-S6ji+flMEga+1QU79NDbwZ8Ivf0S/MpupQQiIC0rTpU/ZTKgcajijJJb1OcByBQDjrXCN1/DJtGz4ZJeBMPGJw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xmldom/xmldom": "0.9.8", + "commander": "13.1.0", + "wicked-good-xpath": "1.3.0" + }, + "bin": { + "sre": "bin/sre" + } + }, + "node_modules/speech-rule-engine/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7772,6 +8132,13 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -7796,6 +8163,13 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/twoslash": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/twoslash/-/twoslash-0.2.12.tgz", @@ -8004,6 +8378,16 @@ "node": ">= 0.4.0" } }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -8244,6 +8628,97 @@ "vue": "^3.0.0" } }, + "node_modules/web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8260,6 +8735,13 @@ "node": ">= 8" } }, + "node_modules/wicked-good-xpath": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", + "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", + "dev": true, + "license": "MIT" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 74ade9ad7b..e78318e615 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "eslint-plugin-vue": "^10.0.0", "fflate": "^0.8.2", "gray-matter": "^4.0.3", + "markdown-it-mathjax3": "^4.3.2", "markdownlint-cli": ">=0.35.0", "markdownlint-rule-search-replace": "^1.1.1", "sass": "^1.62.1",