@sap/cds-compiler 2.10.4 → 2.12.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 (103) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/bin/.eslintrc.json +1 -2
  3. package/bin/cds_update_identifiers.js +10 -8
  4. package/bin/cdsc.js +58 -35
  5. package/bin/cdsse.js +1 -0
  6. package/bin/cdsv2m.js +3 -2
  7. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  8. package/doc/CHANGELOG_BETA.md +16 -0
  9. package/lib/api/.eslintrc.json +2 -0
  10. package/lib/api/main.js +10 -36
  11. package/lib/api/options.js +17 -8
  12. package/lib/api/validate.js +30 -3
  13. package/lib/backends.js +12 -13
  14. package/lib/base/dictionaries.js +2 -1
  15. package/lib/base/keywords.js +3 -2
  16. package/lib/base/message-registry.js +64 -11
  17. package/lib/base/messages.js +38 -18
  18. package/lib/base/model.js +6 -4
  19. package/lib/base/optionProcessorHelper.js +148 -86
  20. package/lib/checks/.eslintrc.json +2 -0
  21. package/lib/checks/actionsFunctions.js +2 -1
  22. package/lib/checks/emptyOrOnlyVirtual.js +2 -2
  23. package/lib/checks/foreignKeys.js +4 -4
  24. package/lib/checks/managedInType.js +4 -4
  25. package/lib/checks/queryNoDbArtifacts.js +1 -3
  26. package/lib/checks/selectItems.js +4 -0
  27. package/lib/checks/sql-snippets.js +93 -0
  28. package/lib/checks/unknownMagic.js +6 -3
  29. package/lib/checks/validator.js +8 -0
  30. package/lib/compiler/assert-consistency.js +14 -5
  31. package/lib/compiler/base.js +64 -0
  32. package/lib/compiler/builtins.js +62 -16
  33. package/lib/compiler/checks.js +34 -10
  34. package/lib/compiler/definer.js +91 -112
  35. package/lib/compiler/index.js +30 -30
  36. package/lib/compiler/propagator.js +8 -4
  37. package/lib/compiler/resolver.js +279 -63
  38. package/lib/compiler/shared.js +65 -230
  39. package/lib/compiler/utils.js +191 -0
  40. package/lib/edm/annotations/genericTranslation.js +35 -18
  41. package/lib/edm/annotations/preprocessAnnotations.js +1 -1
  42. package/lib/edm/csn2edm.js +4 -3
  43. package/lib/edm/edm.js +8 -8
  44. package/lib/edm/edmPreprocessor.js +61 -59
  45. package/lib/edm/edmUtils.js +14 -15
  46. package/lib/gen/Dictionary.json +82 -40
  47. package/lib/gen/language.checksum +1 -1
  48. package/lib/gen/language.interp +19 -1
  49. package/lib/gen/language.tokens +80 -73
  50. package/lib/gen/languageLexer.interp +27 -1
  51. package/lib/gen/languageLexer.js +925 -826
  52. package/lib/gen/languageLexer.tokens +72 -65
  53. package/lib/gen/languageParser.js +4817 -4102
  54. package/lib/json/from-csn.js +57 -26
  55. package/lib/json/to-csn.js +244 -51
  56. package/lib/language/antlrParser.js +12 -1
  57. package/lib/language/docCommentParser.js +1 -1
  58. package/lib/language/errorStrategy.js +26 -8
  59. package/lib/language/genericAntlrParser.js +106 -30
  60. package/lib/language/language.g4 +200 -70
  61. package/lib/language/multiLineStringParser.js +536 -0
  62. package/lib/main.d.ts +220 -21
  63. package/lib/main.js +6 -3
  64. package/lib/model/api.js +2 -2
  65. package/lib/model/csnRefs.js +218 -86
  66. package/lib/model/csnUtils.js +99 -178
  67. package/lib/model/enrichCsn.js +84 -43
  68. package/lib/model/revealInternalProperties.js +25 -8
  69. package/lib/model/sortViews.js +8 -1
  70. package/lib/modelCompare/compare.js +2 -1
  71. package/lib/optionProcessor.js +33 -18
  72. package/lib/render/.eslintrc.json +1 -2
  73. package/lib/render/DuplicateChecker.js +2 -2
  74. package/lib/render/manageConstraints.js +1 -1
  75. package/lib/render/toCdl.js +202 -82
  76. package/lib/render/toHdbcds.js +194 -135
  77. package/lib/render/toRename.js +7 -10
  78. package/lib/render/toSql.js +91 -51
  79. package/lib/render/utils/common.js +24 -5
  80. package/lib/render/utils/sql.js +6 -4
  81. package/lib/transform/braceExpression.js +4 -2
  82. package/lib/transform/db/applyTransformations.js +189 -0
  83. package/lib/transform/db/associations.js +389 -0
  84. package/lib/transform/db/cdsPersistence.js +150 -0
  85. package/lib/transform/db/constraints.js +275 -119
  86. package/lib/transform/db/draft.js +6 -4
  87. package/lib/transform/db/expansion.js +10 -9
  88. package/lib/transform/db/flattening.js +23 -8
  89. package/lib/transform/db/temporal.js +236 -0
  90. package/lib/transform/db/transformExists.js +106 -25
  91. package/lib/transform/db/views.js +485 -0
  92. package/lib/transform/forHanaNew.js +90 -1036
  93. package/lib/transform/forOdataNew.js +11 -3
  94. package/lib/transform/localized.js +5 -14
  95. package/lib/transform/odata/generateForeignKeyElements.js +2 -2
  96. package/lib/transform/transformUtilsNew.js +34 -20
  97. package/lib/transform/translateAssocsToJoins.js +15 -23
  98. package/lib/transform/universalCsnEnricher.js +217 -47
  99. package/lib/utils/file.js +13 -6
  100. package/lib/utils/term.js +65 -42
  101. package/lib/utils/timetrace.js +55 -27
  102. package/package.json +1 -1
  103. package/lib/transform/db/helpers.js +0 -58
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
- getTopLevelArtifactNameOf, getLastPartOf,
4
+ getLastPartOf,
5
5
  getLastPartOfRef,
6
6
  } = require('../model/csnUtils');
7
7
  const {
@@ -10,7 +10,7 @@ const {
10
10
  const keywords = require('../base/keywords');
11
11
  const { renderFunc, beautifyExprArray, findElement } = require('./utils/common');
12
12
  const { checkCSNVersion } = require('../json/csnVersion');
13
- const timetrace = require('../utils/timetrace');
13
+ const { timetrace } = require('../utils/timetrace');
14
14
  const { csnRefs } = require('../model/csnRefs');
15
15
  const { forEachDefinition } = require('../model/csnUtils');
16
16
  const enrichUniversalCsn = require('../transform/universalCsnEnricher');
@@ -40,11 +40,11 @@ function toCdsSourceCsn(csn, options) {
40
40
 
41
41
  checkCSNVersion(csn, options);
42
42
 
43
- const result = Object.create(null);
43
+ const cdlResult = Object.create(null);
44
44
 
45
45
  const main = 'model';
46
46
 
47
- result[main] = `${options.testMode ? '' : `// ${generatedByCompilerVersion()} \n`}`;
47
+ cdlResult[main] = options.testMode ? '' : `// ${generatedByCompilerVersion()} \n`;
48
48
 
49
49
  const subelementAnnotates = [];
50
50
 
@@ -52,7 +52,7 @@ function toCdsSourceCsn(csn, options) {
52
52
  const env = createEnv();
53
53
  const sourceStr = renderArtifact(artifactName, artifact, env); // Must come first because it populates 'env.topLevelAliases'
54
54
  if (sourceStr !== '')
55
- result[main] += `${sourceStr}\n`;
55
+ cdlResult[main] += `${sourceStr}\n`;
56
56
  });
57
57
 
58
58
  // Apply possible subelement/action return annotations with an "annotate X with"
@@ -70,7 +70,7 @@ function toCdsSourceCsn(csn, options) {
70
70
  if (elementName) // action returns do not have element name - we need less {} there
71
71
  sourceStr += ' }\n';
72
72
  sourceStr += '}\n';
73
- result[main] += `${sourceStr}\n`;
73
+ cdlResult[main] += `${sourceStr}\n`;
74
74
  }
75
75
  }
76
76
  }
@@ -117,13 +117,13 @@ function toCdsSourceCsn(csn, options) {
117
117
  sourceStr = renderTypeOrAnnotation(annotationName, anno, env, 'annotation');
118
118
 
119
119
  if (sourceStr !== '')
120
- result[main] += `${sourceStr}\n`;
120
+ cdlResult[main] += `${sourceStr}\n`;
121
121
  }
122
122
  }
123
123
 
124
124
  if (csn.namespace) {
125
- result[csn.namespace] = `namespace ${renderArtifactName(csn.namespace)};\n`;
126
- result[csn.namespace] += `using from './${main}.cds';`;
125
+ cdlResult[csn.namespace] = `namespace ${renderArtifactName(csn.namespace)};\n`;
126
+ cdlResult[csn.namespace] += `using from './${main}.cds';`;
127
127
  }
128
128
 
129
129
 
@@ -132,11 +132,11 @@ function toCdsSourceCsn(csn, options) {
132
132
  if (csn.extensions) {
133
133
  const env = createEnv();
134
134
  const sourceStr = renderUnappliedExtensions(csn.extensions, env);
135
- result.unappliedExtensions = renderUsings('', env) + sourceStr;
135
+ cdlResult.unappliedExtensions = renderUsings('', env) + sourceStr;
136
136
  }
137
137
 
138
138
  timetrace.stop();
139
- return result;
139
+ return cdlResult;
140
140
 
141
141
  /**
142
142
  * Render unapplied 'extend' and 'annotate' statements from the 'extensions array'
@@ -579,7 +579,7 @@ function toCdsSourceCsn(csn, options) {
579
579
  * @return {string}
580
580
  */
581
581
  function renderQueryActionsAndFunctions(artifactName, art, env) {
582
- let result = renderDocComment(art, env) + renderActionsAndFunctions(art, env);
582
+ let result = renderActionsAndFunctions(art, env);
583
583
  // Even if we have seen actions/functions, they might all have been ignored
584
584
  if (result !== '')
585
585
  result = `${env.indent}extend entity ${artifactName} with${result};`;
@@ -612,7 +612,7 @@ function toCdsSourceCsn(csn, options) {
612
612
  }
613
613
  // Now iterate elements - render an annotation if it is different from the column's
614
614
  const childEnv = increaseIndent(env);
615
- let result = renderDocComment(art, env);
615
+ let result = '';
616
616
  for (const elemName in art.elements) {
617
617
  let elemAnnotations = '';
618
618
  const elem = art.elements[elemName];
@@ -996,35 +996,35 @@ function toCdsSourceCsn(csn, options) {
996
996
  /**
997
997
  * Utility function to make sure that we continue with the same indentation in WHERE, GROUP BY, ... after a closing curly brace and beyond
998
998
  *
999
- * @param {string} result
1000
- * @param {CdlRenderEnvironment} env
999
+ * @param {string} str
1000
+ * @param {CdlRenderEnvironment} indentEnv
1001
1001
  * @return {string}
1002
1002
  */
1003
- function continueIndent(result, env) {
1004
- if (result.endsWith('}') || result.endsWith('})')) {
1003
+ function continueIndent(str, indentEnv) {
1004
+ if (str.endsWith('}') || str.endsWith('})')) {
1005
1005
  // The preceding clause ended with '}', just append after that
1006
1006
  return ' ';
1007
1007
  }
1008
1008
  // Otherwise, start new line and indent normally
1009
- return `\n${increaseIndent(env).indent}`;
1009
+ return `\n${increaseIndent(indentEnv).indent}`;
1010
1010
  }
1011
1011
 
1012
1012
  /**
1013
1013
  * Render a query's LIMIT clause, which may have also have OFFSET.
1014
1014
  *
1015
1015
  * @param {CSN.QueryLimit} limit
1016
- * @param {CdlRenderEnvironment} env
1016
+ * @param {CdlRenderEnvironment} limitEnv
1017
1017
  * @return {string}
1018
1018
  */
1019
- function renderLimit(limit, env) {
1020
- let result = '';
1019
+ function renderLimit(limit, limitEnv) {
1020
+ let limitStr = '';
1021
1021
  if (limit.rows !== undefined)
1022
- result += `limit ${renderExpr(limit.rows, env)}`;
1022
+ limitStr += `limit ${renderExpr(limit.rows, limitEnv)}`;
1023
1023
 
1024
1024
  if (limit.offset !== undefined)
1025
- result += `${result !== '' ? `\n${increaseIndent(env).indent}` : ''}offset ${renderExpr(limit.offset, env)}`;
1025
+ limitStr += `${limitStr !== '' ? `\n${increaseIndent(limitEnv).indent}` : ''}offset ${renderExpr(limit.offset, limitEnv)}`;
1026
1026
 
1027
- return result;
1027
+ return limitStr;
1028
1028
  }
1029
1029
  }
1030
1030
 
@@ -1335,7 +1335,7 @@ function toCdsSourceCsn(csn, options) {
1335
1335
  // Primitive: string, number, boolean
1336
1336
 
1337
1337
  // Quote strings, leave all others as they are
1338
- return (typeof x === 'string') ? `'${x.replace(/'/g, '\'\'')}'` : x;
1338
+ return (typeof x === 'string') ? renderString(x, env) : x;
1339
1339
  }
1340
1340
 
1341
1341
  /**
@@ -1385,7 +1385,7 @@ function toCdsSourceCsn(csn, options) {
1385
1385
  case 'timestamp':
1386
1386
  return `${x.literal}'${x.val}'`;
1387
1387
  case 'string':
1388
- return `'${x.val.replace(/'/g, '\'\'')}'`;
1388
+ return renderString(x.val, env);
1389
1389
  case 'object':
1390
1390
  if (x.val === null)
1391
1391
  return 'null';
@@ -1404,6 +1404,12 @@ function toCdsSourceCsn(csn, options) {
1404
1404
  // FIXME: no extra magic with x.param or x.global
1405
1405
  return `${(x.param || x.global) ? ':' : ''}${x.ref.map(renderPathStep).join('.')}`;
1406
1406
  }
1407
+ else if (x.xpr && x.func) {
1408
+ // window function
1409
+ const funcDef = renderFunc( x.func, x, null, a => renderArgs(a, '=>', env) );
1410
+ const windowFunctionOperator = x.xpr.shift(); // OVER ...
1411
+ return `${funcDef} ${windowFunctionOperator} ( ${renderExpr(x.xpr, env, true)} )`;
1412
+ }
1407
1413
  // Function call, possibly with args (use '=>' for named args)
1408
1414
  else if (x.func) {
1409
1415
  // test for non-regular HANA identifier that needs to be quoted
@@ -1636,66 +1642,38 @@ function toCdsSourceCsn(csn, options) {
1636
1642
  }
1637
1643
 
1638
1644
  /**
1639
- * Render a single annotation assignment 'ann' with fully qualified name 'name' (no trailing LF).
1640
- * We might see variants like 'A.B.C#foo' or even 'A.B#foo.C'. In both cases, the #foo must be ignored
1641
- * when resolving the name, but must stay in the rendered output (quoted as necessary).
1645
+ * Render a single annotation assignment 'anno' with fully qualified name 'name' (no trailing LF).
1646
+ * We might see variants like 'A.B.C#foo' or even 'A.B#foo.C'. The latter needs to be quoted as
1647
+ * dots in the variant are not recognized by the compiler.
1642
1648
  *
1643
- * @param {any} ann Annotation value
1649
+ * @param {any} anno Annotation value
1644
1650
  * @param {string} name Annotation name, e.g. `@A.B.C#foo.C`
1645
1651
  * @param {CdlRenderEnvironment} env
1646
- * @return {string}
1652
+ * @return {string} Rendered annotation, possibly quoted: `@![A.B.C#foo.C]: value`
1647
1653
  */
1648
- function renderAnnotationAssignment(ann, name, env) {
1649
- // Take the annotation assignment apart into <nameBeforeVariant>[#<variant>[.<nameAfterVariant>]]
1650
- // @1111111 3333333 5555
1651
- const parts = name.match(/@([^#]+)(#([^.]+)(\.(.+))?)?/);
1652
- const nameBeforeVariant = parts[1];
1653
- const variant = parts[3];
1654
- const nameAfterVariant = parts[5];
1655
- const topLevelName = getTopLevelArtifactNameOf(nameBeforeVariant, csn);
1656
- let result = `${env.indent}@`;
1657
- if (topLevelName) {
1658
- // Checked annotation, with declaration - must render top-level absolute name with proper using and alias
1659
- // Annotation names are never flattened
1660
- result += renderAbsoluteNameWithQuotes(topLevelName);
1661
- if (topLevelName !== nameBeforeVariant)
1662
- result += `.${quotePathString(nameBeforeVariant.substring(topLevelName.length + 1))}`;
1663
- }
1664
- else {
1665
- // Unchecked annotation, just render the name as it is
1666
- result += nameBeforeVariant;
1667
- }
1668
- // Append '#'-variant if any
1669
- if (variant) {
1670
- // FIXME: Unfortunately, the compiler does not yet understand an inner variant with proper quoting,
1671
- // i.e. like "__A"."B"#"foo"."C". As a workaround, we present the '#'-variant as a quoted part of the
1672
- // previous path step, i.e as "__A"."B#foo"."C" (which yields the same result). This hack is only necessary
1673
- // for inner '#'-variants, i.e. for those followed by a <nameAfterVariant>.
1674
- // FIXME: Won't work for inner variants on the top-level artifact, because the USING no longer matches
1675
- // for something like "__A#foo"."B"."C"
1676
- if (nameAfterVariant) {
1677
- const resultSteps = result.split('.');
1678
- // Take all paths steps from the result (which is now essentially 'nameBeforeVariant' with USING
1679
- // adaptations) except the last step
1680
- result = resultSteps.slice(0, -1).join('.');
1681
- // Append a combination of last path step and '#'-variant (quoted)
1682
- let lastStep = resultSteps[resultSteps.length - 1];
1683
- if (lastStep.includes('"')) {
1684
- // Last step was already quoted - strip off the existing quotes
1685
- lastStep = lastStep.slice(1, -1);
1686
- }
1687
- result += `.${quoteIdIfRequired(`${lastStep}#${variant}`)}`;
1688
- }
1689
- else {
1690
- // No hack required for trailing '#'-variant
1691
- result += `#${quoteIdIfRequired(variant)}`;
1692
- }
1693
- }
1694
- // Append anything that might have come after the variant
1695
- if (nameAfterVariant)
1696
- result += `.${quotePathString(nameAfterVariant)}`;
1654
+ function renderAnnotationAssignment(anno, name, env) {
1655
+ name = name.substring(1);
1656
+ // Take the annotation assignment apart into <nameBeforeVariant>#<variantAndRest>
1657
+ const parts = name.split('#');
1658
+ const nameBeforeVariant = parts[0];
1659
+ const variant = parts[1];
1660
+
1661
+ // Identifier according to our grammar: /[$_a-zA-Z][$_a-zA-Z0-9]*/
1662
+ // We expand this pattern to also include dots after the first character.
1663
+ // If the annotation does not follow this pattern `ident(.@ident)*`, it must be quoted:
1664
+ // `@identifier@identifier` must be quoted but `@identifier.@identifier` should not.
1665
+ const annoRequiresQuoting = !/^[$_a-zA-Z][$_a-zA-Z0-9.]*(?:\.@[$_a-zA-Z][$_a-zA-Z0-9.]*)*$/.test(nameBeforeVariant);
1666
+ // Unfortunately, the compiler does not allow `.` after the first variant identifier,
1667
+ // even though that is the result after flattening.
1668
+ const variantRequiresQuoting = variant && !/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(variant);
1669
+
1670
+ let result;
1671
+ if (annoRequiresQuoting || variantRequiresQuoting)
1672
+ result = `${env.indent}@${quote(name)}`;
1673
+ else
1674
+ result = `${env.indent}@${name}`;
1697
1675
 
1698
- result += ` : ${renderAnnotationValue(ann, env)}`;
1676
+ result += ` : ${renderAnnotationValue(anno, env)}`;
1699
1677
  return `${result}\n`;
1700
1678
  }
1701
1679
 
@@ -1742,6 +1720,8 @@ function toCdsSourceCsn(csn, options) {
1742
1720
  topLevelAliases: Object.create(null),
1743
1721
  // Current name prefix (including trailing dot if not empty)
1744
1722
  namePrefix: '',
1723
+ artifactName: null,
1724
+ elementName: null,
1745
1725
  };
1746
1726
  }
1747
1727
 
@@ -1786,11 +1766,21 @@ function toCdsSourceCsn(csn, options) {
1786
1766
  function quoteIdIfRequired(id) {
1787
1767
  // Quote if required for CDL
1788
1768
  if (requiresQuotingForCdl(id))
1789
- return `![${id.replace(/]/g, ']]')}]`;
1769
+ return quote(id);
1790
1770
 
1791
1771
  return id;
1792
1772
  }
1793
1773
 
1774
+ /**
1775
+ * Quotes the identifier using CDL-style ![]-quotes.
1776
+ *
1777
+ * @param id
1778
+ * @returns {string}
1779
+ */
1780
+ function quote(id) {
1781
+ return `![${id.replace(/]/g, ']]')}]`;
1782
+ }
1783
+
1794
1784
  /**
1795
1785
  * Returns true if 'id' requires quotes for CDL, i.e. if 'id'
1796
1786
  * 1. starts with a digit
@@ -1891,6 +1881,136 @@ function toCdsSourceCsn(csn, options) {
1891
1881
  }
1892
1882
  }
1893
1883
 
1884
+ const controlCharacters = /[\u{0000}-\u{0009}\u{000B}\u{001F}]/u;
1885
+ const highSurrogate = /[\u{D800}-\u{DBFF}]/u;
1886
+ const lowSurrogate = /[\u{DC00}-\u{DFFF}]/u;
1887
+
1888
+ /**
1889
+ * Render the given string. Uses back-tick strings.
1890
+ * env is used for indentation for three-back-tick strings.
1891
+ *
1892
+ * @param str
1893
+ * @param env
1894
+ * @returns {string}
1895
+ */
1896
+ function renderString(str, env) {
1897
+ if (isSimpleString(str))
1898
+ return `'${str.replace(/'/g, '\'\'')}'`;
1899
+
1900
+ const output = [];
1901
+
1902
+ // We try to work similar to how JavaScript implements JSON.stringify.
1903
+ // JSON.stringify, however, checks for unpaired unicode surrogates (see §25.5.2.2,
1904
+ // <https://tc39.es/ecma262/#sec-quotejsonstring>), which we do not do here.
1905
+
1906
+ for (let i = 0; i < str.length; ++i) {
1907
+ const char = str[i];
1908
+
1909
+ switch (char) {
1910
+ case '$':
1911
+ output.push('\\$');
1912
+ break;
1913
+ case '`':
1914
+ output.push('\\`');
1915
+ break;
1916
+ case '\\':
1917
+ output.push('\\\\');
1918
+ break;
1919
+ // Replace commonly known escape sequences for control characters
1920
+ // See lib/language/multiLineStringParser.js
1921
+ case '\f':
1922
+ output.push('\\f');
1923
+ break;
1924
+ case '\v':
1925
+ output.push('\\v');
1926
+ break;
1927
+ case '\t':
1928
+ output.push('\\t');
1929
+ break;
1930
+ case '\b':
1931
+ output.push('\\b');
1932
+ break;
1933
+ // If CR, LS, or PS appear, they have been encoded explicitly. If we don't escape
1934
+ // them, a recompilation may yield different results because of newline normalization.
1935
+ case '\r':
1936
+ output.push('\\r');
1937
+ break;
1938
+ case '\u{2028}':
1939
+ output.push('\\u{2028}');
1940
+ break;
1941
+ case '\u{2029}':
1942
+ output.push('\\u{2029}');
1943
+ break;
1944
+ default: {
1945
+ // Control Characters: C0
1946
+ // See <https://en.wikipedia.org/wiki/C0_and_C1_control_codes#Basic_ASCII_control_codes>
1947
+ //
1948
+ // JSON.stringify is not required to escape all control characters, but if used, files may
1949
+ // be interpreted as binary. Therefore, we replace them.
1950
+ //
1951
+ // We exclude LF from this list (\u000A). Characters with "nice" escapes have been replaced above.
1952
+ if (controlCharacters.test(char)) {
1953
+ const hex = char.codePointAt(0).toString(16);
1954
+ output.push(`\\u{${hex}}`);
1955
+ break;
1956
+ }
1957
+
1958
+ // Unicode Surrogates
1959
+ // These characters appear in _pairs_. A high surrogate must be followed by a low surrogate.
1960
+ // If this is not the case, either needs to be encoded. This is also done by JSON.
1961
+ // See also <https://docs.microsoft.com/en-us/globalization/encoding/surrogate-pairs>
1962
+ if (highSurrogate.test(char)) {
1963
+ if (i + 1 >= str.length || !lowSurrogate.test(str[i + 1])) {
1964
+ const hex = char.codePointAt(0).toString(16);
1965
+ output.push(`\\u{${hex}}`);
1966
+ }
1967
+ else {
1968
+ output.push(char);
1969
+ ++i;
1970
+ output.push(str[i]);
1971
+ }
1972
+ }
1973
+ else if (lowSurrogate.test(char)) {
1974
+ const hex = char.codePointAt(0).toString(16);
1975
+ output.push(`\\u{${hex}}`);
1976
+ }
1977
+ else {
1978
+ output.push(char);
1979
+ }
1980
+ }
1981
+ }
1982
+ }
1983
+ str = output.join('');
1984
+ // Note: String is normalized, only \n is the line separator.
1985
+ const lines = str.split('\n');
1986
+ // We don't know whether a text block was used or not. But if there
1987
+ // are more than three lines, text blocks with indentation "look nicer".
1988
+ // This value was chosen by personal taste.
1989
+ if (lines.length > 3) {
1990
+ str = lines.join(`\n${env.indent}`);
1991
+ return `\`\`\`\n${env.indent}${str}\n${env.indent}\`\`\``;
1992
+ }
1993
+
1994
+ return `\`${str}\``;
1995
+ }
1996
+
1997
+ /**
1998
+ * Returns true if the given string can be represented by using single quotes.
1999
+ * @param {string} str
2000
+ */
2001
+ function isSimpleString(str) {
2002
+ // A single-line string allows everything except certain line separators/breaks.
2003
+ // See ANTLR grammar for specifics.
2004
+ // Furthermore, if control characters \u{0000}-\u{001F} are used, we escape them,
2005
+ // as these are explicitly mentioned in the JSON spec (§9):
2006
+ // <https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf>
2007
+ // On top, as (invalid) surrogate pairs need to be handled, we check for them as well.
2008
+ // v3: Not a simple string if ' (\u0027) is in string.
2009
+ return str === '' || (/^[^\u{0000}-\u{001F}\u2028\u2029]+$/u.test(str) &&
2010
+ !highSurrogate.test(str) &&
2011
+ !lowSurrogate.test(str));
2012
+ }
2013
+
1894
2014
  /**
1895
2015
  * @typedef CdlRenderEnvironment Rendering environment used throughout the render process.
1896
2016
  *