@malloydata/malloy 0.0.396 → 0.0.398

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.
@@ -1,12 +1,96 @@
1
1
  import type { Tag } from '@malloydata/malloy-tag';
2
- import type { Annotation } from './model';
2
+ import type { Annotation, DocumentLocation } from './model';
3
3
  import type { LogMessage } from './lang';
4
+ /**
5
+ * @deprecated Argument shape for the deprecated RegExp form of
6
+ * {@link annotationToTag}. The RegExp form cannot see block annotations
7
+ * (`#|`…`|#`). Pass a route string to `annotationToTag` instead, or use the
8
+ * {@link Annotations} view on a tagged entity.
9
+ */
4
10
  export interface TagParseSpec {
5
11
  prefix?: RegExp;
6
12
  }
13
+ /** One annotation, unparsed — its raw text and where its content begins. */
14
+ export interface AnnotationText {
15
+ /** The annotation exactly as written — prefix + content. */
16
+ rawText: string;
17
+ /** Offset where the content begins; `rawText.slice(contentIndex)` is the content. */
18
+ contentIndex: number;
19
+ /** Where `rawText` begins in the source document. */
20
+ at: DocumentLocation;
21
+ /**
22
+ * For block annotations: characters of leading whitespace removed from
23
+ * each body line by the translator's dedent pass. A BYO parser that wants
24
+ * source-mapped error columns adds this to the parser's reported column for
25
+ * body lines (`source_col = indentStripped + parser_col`).
26
+ */
27
+ indentStripped?: number;
28
+ }
29
+ /** An {@link AnnotationText} that also carries its route (`''` is MOTLY). */
30
+ export interface RoutedAnnotation extends AnnotationText {
31
+ route: string;
32
+ }
33
+ /**
34
+ * Collect annotations, using the shared prefix parser.
35
+ * - no `route`: every annotation, each carrying its own `route` (the only way
36
+ * to reach one whose prefix is malformed).
37
+ * - a `route`: only annotations on that route, `route` omitted from each result
38
+ * (you passed it); malformed prefixes excluded.
39
+ */
40
+ export declare function collectAnnotations(annote: Annotation | undefined): RoutedAnnotation[];
41
+ export declare function collectAnnotations(annote: Annotation | undefined, route: string): AnnotationText[];
42
+ /**
43
+ * @deprecated The RegExp form cannot see block annotations (`#|`…`|#`). Use
44
+ * `new Annotations(annote).texts(route)` instead, or the {@link Annotations}
45
+ * view on a tagged entity (`entity.annotations.texts(route)`).
46
+ */
7
47
  export declare function annotationToTaglines(annote: Annotation | undefined, prefix?: RegExp): string[];
8
48
  export interface MalloyTagParse {
9
49
  tag: Tag;
10
50
  log: LogMessage[];
11
51
  }
52
+ /** Parse the annotations on `route` (default `''`, the MOTLY tag route) as MOTLY. */
53
+ export declare function annotationToTag(annote: Annotation | undefined, route?: string): MalloyTagParse;
54
+ /**
55
+ * @deprecated The RegExp `prefix` form cannot see block annotations
56
+ * (`#|`…`|#`) and cannot report content offsets for error mapping. Pass a route
57
+ * string (the other overload), or use {@link Annotations.parseAsTag} on a
58
+ * tagged entity.
59
+ */
12
60
  export declare function annotationToTag(annote: Annotation | undefined, spec?: TagParseSpec): MalloyTagParse;
61
+ /**
62
+ * The route-aware annotation API for a tagged entity.
63
+ *
64
+ * An annotation has a *prefix* (everything from `#`/`##` up to the first
65
+ * whitespace) that resolves to a *route* — a namespace key. Built-in routes:
66
+ * `''` (MOTLY tags, the human default), `!` (compiler flags), `@` (persistence
67
+ * directives), `"` (doc-string markdown). Apps stake their own routes with
68
+ * brackets: `#(myApp) ...` is route `myApp`. The grammar (forms, bracket
69
+ * pairs, malformation warnings) lives in `./prefix.ts`.
70
+ *
71
+ * All annotation reading lives here, written once; each tagged class only has
72
+ * to say *where* its annotation is (by handing it to the constructor). Unlike
73
+ * the deprecated RegExp readers (`tagParse`/`getTaglines`), this sees block
74
+ * annotations.
75
+ */
76
+ export declare class Annotations {
77
+ private readonly annote;
78
+ constructor(annote: Annotation | undefined);
79
+ /**
80
+ * Raw annotation text strings (prefix + content) — all routes if `route` is
81
+ * omitted, just that route's otherwise. The route-based successor to the
82
+ * deprecated `getTaglines`. For source-mapped offsets (bring-your-own
83
+ * parsers), see {@link forRoute}.
84
+ */
85
+ texts(route?: string): string[];
86
+ /**
87
+ * Your route's annotations as objects (`rawText` + `contentIndex` + `at`) —
88
+ * the bring-your-own-parser door. A non-MOTLY app (e.g. JSON on its own
89
+ * route) reads these to slice the content (`rawText.slice(contentIndex)`)
90
+ * itself and map its parser's errors back to source via `at`. Malformed-prefix
91
+ * annotations are excluded.
92
+ */
93
+ forRoute(route: string): AnnotationText[];
94
+ /** Parse a route's annotations as a MOTLY tag. Default `''` is the tag route. */
95
+ parseAsTag(route?: string): MalloyTagParse;
96
+ }
@@ -1,50 +1,80 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Annotations = void 0;
4
+ exports.collectAnnotations = collectAnnotations;
3
5
  exports.annotationToTaglines = annotationToTaglines;
4
6
  exports.annotationToTag = annotationToTag;
5
7
  const malloy_tag_1 = require("@malloydata/malloy-tag");
8
+ const prefix_1 = require("./prefix");
9
+ /** Every Note of an annotation, inherited first, in document order. */
10
+ function* notesInOrder(annote) {
11
+ if (annote.inherits)
12
+ yield* notesInOrder(annote.inherits);
13
+ if (annote.blockNotes)
14
+ yield* annote.blockNotes;
15
+ if (annote.notes)
16
+ yield* annote.notes;
17
+ }
18
+ function collectAnnotations(annote, route) {
19
+ if (route === undefined) {
20
+ return Array.from(notesInOrder(annote !== null && annote !== void 0 ? annote : {}), note => {
21
+ const { route: noteRoute, contentIndex } = (0, prefix_1.parsePrefix)(note.text);
22
+ return {
23
+ rawText: note.text,
24
+ contentIndex,
25
+ at: note.at,
26
+ route: noteRoute,
27
+ indentStripped: note.indentStripped,
28
+ };
29
+ });
30
+ }
31
+ const matching = [];
32
+ for (const note of notesInOrder(annote !== null && annote !== void 0 ? annote : {})) {
33
+ const parsed = (0, prefix_1.parsePrefix)(note.text);
34
+ if (parsed.route === route && parsed.malformation !== 'malformed-route') {
35
+ matching.push({
36
+ rawText: note.text,
37
+ contentIndex: parsed.contentIndex,
38
+ at: note.at,
39
+ indentStripped: note.indentStripped,
40
+ });
41
+ }
42
+ }
43
+ return matching;
44
+ }
6
45
  /**
7
46
  * Collect all matching Notes from an Annotation, walking the inherits
8
47
  * chain. Returns notes in inheritance order (inherited first).
48
+ *
49
+ * @deprecated RegExp prefix matching; use {@link collectAnnotations} with a route.
9
50
  */
10
51
  function collectNotes(annote, prefix) {
11
- const inherited = annote.inherits
12
- ? collectNotes(annote.inherits, prefix)
13
- : [];
14
- const allNotes = [];
15
- if (annote.blockNotes) {
16
- allNotes.push(...annote.blockNotes);
17
- }
18
- if (annote.notes) {
19
- allNotes.push(...annote.notes);
20
- }
21
- if (prefix) {
22
- const matching = allNotes.filter(note => note.text.match(prefix));
23
- return inherited.concat(matching);
24
- }
25
- return inherited.concat(allNotes);
52
+ const notes = [...notesInOrder(annote)];
53
+ return prefix ? notes.filter(note => note.text.match(prefix)) : notes;
26
54
  }
55
+ /**
56
+ * @deprecated The RegExp form cannot see block annotations (`#|`…`|#`). Use
57
+ * `new Annotations(annote).texts(route)` instead, or the {@link Annotations}
58
+ * view on a tagged entity (`entity.annotations.texts(route)`).
59
+ */
27
60
  function annotationToTaglines(annote, prefix) {
28
61
  return collectNotes(annote || {}, prefix).map(n => n.text);
29
62
  }
30
- function annotationToTag(annote, spec = {}) {
31
- const prefix = spec.prefix || /^##? /;
32
- annote || (annote = {});
33
- const notes = collectNotes(annote, prefix);
63
+ /** Parse a run of Notes as MOTLY into one Tag, collecting errors. */
64
+ function parseTaglines(lines) {
34
65
  const allErrs = [];
35
66
  const session = new malloy_tag_1.TagParser();
36
- for (const note of notes) {
67
+ for (const line of lines) {
37
68
  const origin = {
38
- url: note.at.url,
39
- startLine: note.at.range.start.line,
40
- startColumn: note.at.range.start.character,
69
+ url: line.at.url,
70
+ startLine: line.at.range.start.line,
71
+ startColumn: line.at.range.start.character,
41
72
  };
42
- const noteParse = session.parseAnnotation(note.text, origin);
43
- allErrs.push(...noteParse.log.map((e) => mapMalloyError(e, note)));
73
+ const noteParse = session.parseAnnotation(line.text, origin);
74
+ allErrs.push(...noteParse.log.map((e) => mapMalloyError(e, line)));
44
75
  }
45
76
  const tag = session.finish();
46
- const refErrors = tag.validateReferences();
47
- for (const refError of refErrors) {
77
+ for (const refError of tag.validateReferences()) {
48
78
  allErrs.push({
49
79
  code: 'tag-reference-error',
50
80
  severity: 'warn',
@@ -53,25 +83,78 @@ function annotationToTag(annote, spec = {}) {
53
83
  }
54
84
  return { tag, log: allErrs };
55
85
  }
56
- function mapMalloyError(e, note) {
57
- // Calculate prefix length (same logic as stripPrefix in malloy-tag)
58
- let prefixLen = 0;
59
- if (note.text[0] === '#') {
60
- const skipTo = note.text.search(/[ \n]/);
61
- if (skipTo > 0) {
62
- prefixLen = skipTo;
63
- }
86
+ function annotationToTag(annote, arg) {
87
+ if (typeof arg === 'object') {
88
+ const prefix = arg.prefix || /^##? /;
89
+ return parseTaglines(collectNotes(annote !== null && annote !== void 0 ? annote : {}, prefix));
90
+ }
91
+ const matched = collectAnnotations(annote, arg !== null && arg !== void 0 ? arg : '');
92
+ return parseTaglines(matched.map(a => ({
93
+ text: a.rawText,
94
+ at: a.at,
95
+ indentStripped: a.indentStripped,
96
+ })));
97
+ }
98
+ /**
99
+ * The route-aware annotation API for a tagged entity.
100
+ *
101
+ * An annotation has a *prefix* (everything from `#`/`##` up to the first
102
+ * whitespace) that resolves to a *route* — a namespace key. Built-in routes:
103
+ * `''` (MOTLY tags, the human default), `!` (compiler flags), `@` (persistence
104
+ * directives), `"` (doc-string markdown). Apps stake their own routes with
105
+ * brackets: `#(myApp) ...` is route `myApp`. The grammar (forms, bracket
106
+ * pairs, malformation warnings) lives in `./prefix.ts`.
107
+ *
108
+ * All annotation reading lives here, written once; each tagged class only has
109
+ * to say *where* its annotation is (by handing it to the constructor). Unlike
110
+ * the deprecated RegExp readers (`tagParse`/`getTaglines`), this sees block
111
+ * annotations.
112
+ */
113
+ class Annotations {
114
+ constructor(annote) {
115
+ this.annote = annote;
64
116
  }
65
- // Map error position to source location
66
- // e.line is 0-based line within the (stripped) input
67
- // e.offset is 0-based column within that line
68
- // TODO: For block annotations, lines > 0 have indentation stripped by
69
- // stripBlockIndent, so e.offset doesn't account for the removed columns.
70
- // This makes error squigglies misaligned on block annotation body lines.
117
+ /**
118
+ * Raw annotation text strings (prefix + content) — all routes if `route` is
119
+ * omitted, just that route's otherwise. The route-based successor to the
120
+ * deprecated `getTaglines`. For source-mapped offsets (bring-your-own
121
+ * parsers), see {@link forRoute}.
122
+ */
123
+ texts(route) {
124
+ const items = route === undefined
125
+ ? collectAnnotations(this.annote)
126
+ : collectAnnotations(this.annote, route);
127
+ return items.map(a => a.rawText);
128
+ }
129
+ /**
130
+ * Your route's annotations as objects (`rawText` + `contentIndex` + `at`) —
131
+ * the bring-your-own-parser door. A non-MOTLY app (e.g. JSON on its own
132
+ * route) reads these to slice the content (`rawText.slice(contentIndex)`)
133
+ * itself and map its parser's errors back to source via `at`. Malformed-prefix
134
+ * annotations are excluded.
135
+ */
136
+ forRoute(route) {
137
+ return collectAnnotations(this.annote, route);
138
+ }
139
+ /** Parse a route's annotations as a MOTLY tag. Default `''` is the tag route. */
140
+ parseAsTag(route = '') {
141
+ return annotationToTag(this.annote, route);
142
+ }
143
+ }
144
+ exports.Annotations = Annotations;
145
+ function mapMalloyError(e, note) {
146
+ var _a;
147
+ // MOTLY reports `e.line` / `e.offset` into the *stripped* note text it
148
+ // parsed. To map back to source:
149
+ // line 0 (opener line): col = opener_col + prefix_len + e.offset
150
+ // line N>0 (body lines): col = indentStripped + e.offset
151
+ // `indentStripped` is the per-line dedent recorded on the Note by the
152
+ // translator (uniform per block, so the same formula serves every body
153
+ // line). Prefix length is everything before the separator, via parsePrefix.
71
154
  const line = note.at.range.start.line + e.line;
72
155
  const character = e.line === 0
73
- ? note.at.range.start.character + prefixLen + e.offset
74
- : e.offset;
156
+ ? note.at.range.start.character + prefixLength(note.text) + e.offset
157
+ : ((_a = note.indentStripped) !== null && _a !== void 0 ? _a : 0) + e.offset;
75
158
  const loc = { line, character };
76
159
  return {
77
160
  code: 'tag-parse-error',
@@ -79,11 +162,14 @@ function mapMalloyError(e, note) {
79
162
  message: e.message,
80
163
  at: {
81
164
  url: note.at.url,
82
- range: {
83
- start: loc,
84
- end: loc,
85
- },
165
+ range: { start: loc, end: loc },
86
166
  },
87
167
  };
88
168
  }
169
+ /** Length of the annotation prefix per malloy-tag's `stripPrefix`: index of
170
+ * the first whitespace, or 0 if none. */
171
+ function prefixLength(text) {
172
+ const { contentIndex } = (0, prefix_1.parsePrefix)(text);
173
+ return contentIndex === text.length ? 0 : contentIndex - 1;
174
+ }
89
175
  //# sourceMappingURL=annotation.js.map
package/dist/api/core.js CHANGED
@@ -56,7 +56,6 @@ const lang_1 = require("../lang");
56
56
  const model_1 = require("../model");
57
57
  const to_stable_1 = require("../to_stable");
58
58
  const sql_block_1 = require("../model/sql_block");
59
- const annotation_1 = require("../annotation");
60
59
  const malloy_tag_1 = require("@malloydata/malloy-tag");
61
60
  const util_1 = require("./util");
62
61
  const timing_1 = require("../timing");
@@ -506,9 +505,7 @@ function statedCompileQuery(state) {
506
505
  defaultRowLimit: state.defaultRowLimit,
507
506
  });
508
507
  timer.contribute([sqlTimer.stop()]);
509
- const modelAnnotations = (0, annotation_1.annotationToTaglines)(result.modelDef.annotation).map(l => ({
510
- value: l,
511
- }));
508
+ const modelAnnotations = (0, to_stable_1.toStableAnnotations)(result.modelDef.annotation);
512
509
  let source;
513
510
  if (query.compositeResolvedSourceDef) {
514
511
  source = query.compositeResolvedSourceDef;
@@ -522,9 +519,7 @@ function statedCompileQuery(state) {
522
519
  source = (0, model_1.safeRecordGet)(result.modelDef.contents, query.structRef);
523
520
  }
524
521
  }
525
- const sourceAnnotations = (0, annotation_1.annotationToTaglines)(source.annotation).map(l => ({
526
- value: l,
527
- }));
522
+ const sourceAnnotations = (0, to_stable_1.toStableAnnotations)(source.annotation);
528
523
  const sourceMetadataTag = malloy_tag_1.Tag.withPrefix('#(malloy) ');
529
524
  sourceMetadataTag.set(['source', 'name'], translatedQuery.sourceExplore);
530
525
  const sourceArguments = (_b = translatedQuery.sourceArguments) !== null && _b !== void 0 ? _b : ((0, model_1.isSourceDef)(source) ? source.arguments : undefined);
@@ -5,6 +5,7 @@ import type { Dialect } from '../../dialect';
5
5
  import type { BuildGraph, CompileQueryOptions } from './types';
6
6
  import { Tag } from '@malloydata/malloy-tag';
7
7
  import type { MalloyTagParse, TagParseSpec } from '../../annotation';
8
+ import { Annotations } from '../../annotation';
8
9
  import type * as Malloy from '@malloydata/malloy-interfaces';
9
10
  import type { Taggable } from '../../taggable';
10
11
  declare abstract class Entity {
@@ -101,6 +102,7 @@ export declare class Explore extends Entity implements Taggable {
101
102
  isExploreField(): this is ExploreField;
102
103
  tagParse(spec?: TagParseSpec): MalloyTagParse;
103
104
  getTaglines(prefix?: RegExp): string[];
105
+ get annotations(): Annotations;
104
106
  private parsedModelTag?;
105
107
  get modelTag(): Tag;
106
108
  /**
@@ -141,6 +143,7 @@ export declare class AtomicField extends Entity implements Taggable {
141
143
  get type(): AtomicFieldType;
142
144
  tagParse(spec?: TagParseSpec): MalloyTagParse;
143
145
  getTaglines(prefix?: RegExp): string[];
146
+ get annotations(): Annotations;
144
147
  isIntrinsic(): boolean;
145
148
  isQueryField(): this is QueryField;
146
149
  isExploreField(): this is ExploreField;
@@ -208,6 +211,7 @@ export declare class QueryField extends Query implements Taggable {
208
211
  constructor(turtleDef: TurtleDef, parent: Explore, source?: Query);
209
212
  tagParse(spec?: TagParseSpec): MalloyTagParse;
210
213
  getTaglines(prefix?: RegExp): string[];
214
+ get annotations(): Annotations;
211
215
  isQueryField(): this is QueryField;
212
216
  isExploreField(): this is ExploreField;
213
217
  isAtomicField(): this is AtomicField;
@@ -223,6 +227,7 @@ export declare class ExploreField extends Explore {
223
227
  get isRecord(): boolean;
224
228
  get isArray(): boolean;
225
229
  tagParse(spec?: TagParseSpec): MalloyTagParse;
230
+ get annotations(): Annotations;
226
231
  isQueryField(): this is QueryField;
227
232
  isExploreField(): this is ExploreField;
228
233
  isAtomicField(): this is AtomicField;
@@ -282,6 +287,7 @@ export declare class Model implements Taggable {
282
287
  get givens(): ReadonlyMap<string, Given>;
283
288
  tagParse(spec?: TagParseSpec): MalloyTagParse;
284
289
  getTaglines(prefix?: RegExp): string[];
290
+ get annotations(): Annotations;
285
291
  /**
286
292
  * Retrieve a document reference for the token at the given position within
287
293
  * the document that produced this model.
@@ -419,6 +425,7 @@ export declare class PersistSource implements Taggable {
419
425
  * Get annotation taglines matching an optional prefix.
420
426
  */
421
427
  getTaglines(prefix?: RegExp): string[];
428
+ get annotations(): Annotations;
422
429
  /**
423
430
  * The connection name for this source.
424
431
  */
@@ -484,6 +491,7 @@ export declare class Given implements Taggable {
484
491
  get location(): DocumentLocation | undefined;
485
492
  tagParse(spec?: TagParseSpec): MalloyTagParse;
486
493
  getTaglines(prefix?: RegExp): string[];
494
+ get annotations(): Annotations;
487
495
  }
488
496
  export declare class PreparedQuery implements Taggable {
489
497
  private _model;
@@ -494,6 +502,7 @@ export declare class PreparedQuery implements Taggable {
494
502
  get _modelDef(): ModelDef;
495
503
  tagParse(spec?: TagParseSpec): MalloyTagParse;
496
504
  getTaglines(prefix?: RegExp): string[];
505
+ get annotations(): Annotations;
497
506
  /**
498
507
  * Generate the SQL for this query.
499
508
  *
@@ -537,6 +546,7 @@ export declare class PreparedResult implements Taggable {
537
546
  static fromJson({ query, modelDef, }: PreparedResultJSON): PreparedResult;
538
547
  tagParse(spec?: TagParseSpec): MalloyTagParse;
539
548
  getTaglines(prefix?: RegExp): string[];
549
+ get annotations(): Annotations;
540
550
  get annotation(): Annotation | undefined;
541
551
  get modelAnnotation(): Annotation | undefined;
542
552
  get modelTag(): Tag;
@@ -154,8 +154,11 @@ class Explore extends Entity {
154
154
  getTaglines(prefix) {
155
155
  return (0, annotation_1.annotationToTaglines)(this._structDef.annotation, prefix);
156
156
  }
157
+ get annotations() {
158
+ return new annotation_1.Annotations(this._structDef.annotation);
159
+ }
157
160
  get modelTag() {
158
- this.parsedModelTag || (this.parsedModelTag = (0, annotation_1.annotationToTag)(this._structDef.modelAnnotation).tag);
161
+ this.parsedModelTag || (this.parsedModelTag = new annotation_1.Annotations(this._structDef.modelAnnotation).parseAsTag().tag);
159
162
  return this.parsedModelTag;
160
163
  }
161
164
  /**
@@ -465,6 +468,9 @@ class AtomicField extends Entity {
465
468
  getTaglines(prefix) {
466
469
  return (0, annotation_1.annotationToTaglines)(this.fieldTypeDef.annotation, prefix);
467
470
  }
471
+ get annotations() {
472
+ return new annotation_1.Annotations(this.fieldTypeDef.annotation);
473
+ }
468
474
  isIntrinsic() {
469
475
  return (0, model_1.fieldIsIntrinsic)(this.fieldTypeDef);
470
476
  }
@@ -666,6 +672,9 @@ class QueryField extends Query {
666
672
  getTaglines(prefix) {
667
673
  return (0, annotation_1.annotationToTaglines)(this.turtleDef.annotation, prefix);
668
674
  }
675
+ get annotations() {
676
+ return new annotation_1.Annotations(this.turtleDef.annotation);
677
+ }
669
678
  isQueryField() {
670
679
  return true;
671
680
  }
@@ -719,6 +728,9 @@ class ExploreField extends Explore {
719
728
  tagParse(spec) {
720
729
  return (0, annotation_1.annotationToTag)(this._structDef.annotation, spec);
721
730
  }
731
+ get annotations() {
732
+ return new annotation_1.Annotations(this._structDef.annotation);
733
+ }
722
734
  isQueryField() {
723
735
  return false;
724
736
  }
@@ -807,6 +819,9 @@ class Model {
807
819
  getTaglines(prefix) {
808
820
  return (0, annotation_1.annotationToTaglines)(this.modelDef.annotation, prefix);
809
821
  }
822
+ get annotations() {
823
+ return new annotation_1.Annotations(this.modelDef.annotation);
824
+ }
810
825
  /**
811
826
  * Retrieve a document reference for the token at the given position within
812
827
  * the document that produced this model.
@@ -932,7 +947,7 @@ class Model {
932
947
  */
933
948
  getBuildPlan() {
934
949
  // Require experimental.persistence compiler flag
935
- const modelTag = this.tagParse({ prefix: /^##! / }).tag;
950
+ const modelTag = this.annotations.parseAsTag('!').tag;
936
951
  if (!modelTag.has('experimental', 'persistence')) {
937
952
  throw new Error('Model must have ##! experimental.persistence to use getBuildPlan()');
938
953
  }
@@ -1057,6 +1072,9 @@ class PersistSource {
1057
1072
  getTaglines(prefix) {
1058
1073
  return this.explore.getTaglines(prefix);
1059
1074
  }
1075
+ get annotations() {
1076
+ return this.explore.annotations;
1077
+ }
1060
1078
  /**
1061
1079
  * The connection name for this source.
1062
1080
  */
@@ -1159,6 +1177,9 @@ class Given {
1159
1177
  getTaglines(prefix) {
1160
1178
  return (0, annotation_1.annotationToTaglines)(this._internal.annotation, prefix);
1161
1179
  }
1180
+ get annotations() {
1181
+ return new annotation_1.Annotations(this._internal.annotation);
1182
+ }
1162
1183
  }
1163
1184
  exports.Given = Given;
1164
1185
  class PreparedQuery {
@@ -1177,6 +1198,9 @@ class PreparedQuery {
1177
1198
  getTaglines(prefix) {
1178
1199
  return (0, annotation_1.annotationToTaglines)(this._query.annotation, prefix);
1179
1200
  }
1201
+ get annotations() {
1202
+ return new annotation_1.Annotations(this._query.annotation);
1203
+ }
1180
1204
  /**
1181
1205
  * Generate the SQL for this query.
1182
1206
  *
@@ -1281,6 +1305,9 @@ class PreparedResult {
1281
1305
  getTaglines(prefix) {
1282
1306
  return (0, annotation_1.annotationToTaglines)(this.inner.annotation, prefix);
1283
1307
  }
1308
+ get annotations() {
1309
+ return new annotation_1.Annotations(this.inner.annotation);
1310
+ }
1284
1311
  get annotation() {
1285
1312
  return this.inner.annotation;
1286
1313
  }
@@ -1288,7 +1315,7 @@ class PreparedResult {
1288
1315
  return this.modelDef.annotation;
1289
1316
  }
1290
1317
  get modelTag() {
1291
- return (0, annotation_1.annotationToTag)(this.modelDef.annotation).tag;
1318
+ return new annotation_1.Annotations(this.modelDef.annotation).parseAsTag().tag;
1292
1319
  }
1293
1320
  /**
1294
1321
  * @return The name of the connection this query should be run against.
@@ -1361,9 +1388,7 @@ class PreparedResult {
1361
1388
  const structs = this.inner.structs;
1362
1389
  const struct = structs[structs.length - 1];
1363
1390
  const schema = { fields: (0, to_stable_1.convertFieldInfos)(struct, struct.fields) };
1364
- const annotations = (0, annotation_1.annotationToTaglines)(this.inner.annotation).map(l => ({
1365
- value: l,
1366
- }));
1391
+ const annotations = (0, to_stable_1.toStableAnnotations)(this.inner.annotation);
1367
1392
  const metadataAnnot = struct.resultMetadata
1368
1393
  ? (0, to_stable_1.getResultStructMetadataAnnotation)(struct, struct.resultMetadata)
1369
1394
  : undefined;
@@ -1392,9 +1417,7 @@ class PreparedResult {
1392
1417
  .set(['query_name'], this.inner.queryName || struct.name)
1393
1418
  .toString(),
1394
1419
  });
1395
- const modelAnnotations = (0, annotation_1.annotationToTaglines)(this.modelDef.annotation).map(l => ({
1396
- value: l,
1397
- }));
1420
+ const modelAnnotations = (0, to_stable_1.toStableAnnotations)(this.modelDef.annotation);
1398
1421
  return {
1399
1422
  schema,
1400
1423
  data,
@@ -1010,7 +1010,7 @@ class QueryMaterializer extends FluentState {
1010
1010
  buildManifest = undefined;
1011
1011
  }
1012
1012
  if (buildManifest) {
1013
- const modelTag = preparedQuery.model.tagParse({ prefix: /^##! / }).tag;
1013
+ const modelTag = preparedQuery.model.annotations.parseAsTag('!').tag;
1014
1014
  if (!modelTag.has('experimental', 'persistence')) {
1015
1015
  if (explicitManifest) {
1016
1016
  // Explicitly passed non-empty manifest requires persistence support
@@ -547,7 +547,7 @@ function getPartitionCompositeFilter(partitionComposite, fieldUsage) {
547
547
  function getPartitionCompositeDesc(annotation, structDef, logTo) {
548
548
  if (annotation === undefined)
549
549
  return undefined;
550
- const compilerFlags = (0, annotation_1.annotationToTag)(annotation, { prefix: /^#!\s*/ }).tag;
550
+ const compilerFlags = new annotation_1.Annotations(annotation).parseAsTag('!').tag;
551
551
  const partitionCompositeTag = compilerFlags.tag('experimental', 'partition_composite');
552
552
  if (partitionCompositeTag === undefined)
553
553
  return undefined;
@@ -92,6 +92,13 @@ export declare class MalloyToAST extends AbstractParseTreeVisitor<ast.MalloyElem
92
92
  */
93
93
  protected parseTime(pcx: ParserRuleContext, parse: (s: string) => ast.ExpressionDef | undefined): ast.ExpressionDef;
94
94
  protected getAnnotation(cx: parse.AnnotationContext): Note;
95
+ /**
96
+ * Warn if the annotation prefix is not a well-formed route. The note is still
97
+ * stored either way — the malformation only drives the diagnostic, never the
98
+ * IR. Warnings fire at note construction; inherited annotations carry no
99
+ * malformation marker through the IR and are not re-warned by importers.
100
+ */
101
+ private warnIfMalformedPrefix;
95
102
  protected getNotes(cx: HasAnnotations): Note[];
96
103
  protected getIsNotes(cx: parse.IsDefineContext): Note[];
97
104
  visitMalloyDocument(pcx: parse.MalloyDocumentContext): ast.Document;
@@ -62,6 +62,7 @@ const AbstractParseTreeVisitor_1 = require("antlr4ts/tree/AbstractParseTreeVisit
62
62
  const ast = __importStar(require("./ast"));
63
63
  const parse_log_1 = require("./parse-log");
64
64
  const Interval_1 = require("antlr4ts/misc/Interval");
65
+ const prefix_1 = require("../prefix");
65
66
  const ast_1 = require("./ast");
66
67
  const parse_utils_1 = require("./parse-utils");
67
68
  const malloy_types_1 = require("../model/malloy_types");
@@ -271,12 +272,26 @@ class MalloyToAST extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
271
272
  return this.astAt(def, pcx);
272
273
  }
273
274
  getAnnotation(cx) {
274
- const text = (0, parse_utils_1.getAnnotationText)(cx, (wcx, msg) => {
275
- this.contextError(wcx, 'block-annotation-warning', msg, {
276
- severity: 'warn',
277
- });
278
- });
279
- return { text: text, at: this.getLocation(cx) };
275
+ const note = (0, parse_utils_1.noteFromAnnotation)(cx, this.parseInfo);
276
+ this.warnIfMalformedPrefix(note.text, cx);
277
+ return note;
278
+ }
279
+ /**
280
+ * Warn if the annotation prefix is not a well-formed route. The note is still
281
+ * stored either way — the malformation only drives the diagnostic, never the
282
+ * IR. Warnings fire at note construction; inherited annotations carry no
283
+ * malformation marker through the IR and are not re-warned by importers.
284
+ */
285
+ warnIfMalformedPrefix(text, cx) {
286
+ const parsed = (0, prefix_1.parsePrefix)(text);
287
+ if (parsed.malformation === undefined)
288
+ return;
289
+ // The slice up to contentIndex is "prefix + separator"; trim trailing
290
+ // whitespace to land on the prefix the user wrote. (A no-content single-
291
+ // line note like `#malformed\n` exposes this: contentIndex === text.length
292
+ // but the slice still ends at the `\n`.)
293
+ const prefix = text.slice(0, parsed.contentIndex).replace(/\s+$/, '');
294
+ this.contextError(cx, parsed.malformation, { prefix });
280
295
  }
281
296
  getNotes(cx) {
282
297
  return cx.annotation().map(a => this.getAnnotation(a));
@@ -1533,10 +1548,11 @@ class MalloyToAST extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
1533
1548
  this.contextError(pcx, 'unclosed-block-annotation', 'Block annotation is not closed, add correctly indented "|##"');
1534
1549
  }
1535
1550
  }
1536
- const allNotes = pcx.docAnnotation().map(a => ({
1537
- text: (0, parse_utils_1.getAnnotationText)(a),
1538
- at: this.getLocation(pcx),
1539
- }));
1551
+ const allNotes = pcx.docAnnotation().map(a => {
1552
+ const note = (0, parse_utils_1.noteFromAnnotation)(a, this.parseInfo);
1553
+ this.warnIfMalformedPrefix(note.text, a);
1554
+ return note;
1555
+ });
1540
1556
  const tags = new ast.ModelAnnotation(allNotes);
1541
1557
  this.updateCompilerFlags(tags);
1542
1558
  return tags;
@@ -101,9 +101,9 @@ class MalloyToQuery extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor
101
101
  * @returns Array of texts for the annotations
102
102
  */
103
103
  getAnnotations(cx) {
104
- const annotations = cx.annotation().map(a => {
105
- return { value: (0, parse_utils_1.getAnnotationText)(a) };
106
- });
104
+ const annotations = cx.annotation().map(a => ({
105
+ value: (0, parse_utils_1.getAnnotationText)(a),
106
+ }));
107
107
  return annotations.length > 0 ? annotations : undefined;
108
108
  }
109
109
  getIsAnnotations(cx) {
@@ -358,7 +358,12 @@ type MessageParameterTypes = {
358
358
  'failed-to-parse-function-name': string;
359
359
  'orphaned-object-annotation': string;
360
360
  'unclosed-block-annotation': string;
361
- 'block-annotation-warning': string;
361
+ 'malformed-route': {
362
+ prefix: string;
363
+ };
364
+ 'reserved-route': {
365
+ prefix: string;
366
+ };
362
367
  'misplaced-model-annotation': string;
363
368
  'unexpected-non-source-query-expression-node': string;
364
369
  'sql-not-like': string;
@@ -105,6 +105,14 @@ exports.MESSAGE_FORMATTERS = {
105
105
  message: e,
106
106
  tag: 'restricted-mode',
107
107
  }),
108
+ 'malformed-route': e => ({
109
+ message: `Annotation prefix \`${e.prefix}\` is not a well-formed route — write \`# ...\` for a tag (note the space) or \`#(name)\` for an app route`,
110
+ severity: 'warn',
111
+ }),
112
+ 'reserved-route': e => ({
113
+ message: `Annotation prefix \`${e.prefix}\` uses an unclaimed sigil; punct-only prefixes are reserved for Malloy's own use`,
114
+ severity: 'warn',
115
+ }),
108
116
  };
109
117
  function makeLogMessage(code, parameters, options) {
110
118
  var _a, _b, _c, _d, _e;
@@ -478,7 +478,7 @@ class TranslateStep {
478
478
  // seeding (e.g. TestTranslator's compilerFlags option) survive.
479
479
  if (extendingModel && !this.importedAnnotations) {
480
480
  const parseCompilerFlagsTimer = new timing_1.Timer('parse_compiler_flags');
481
- that.compilerFlagSrc.push(...(0, annotation_1.annotationToTaglines)(extendingModel.annotation, /^##! /));
481
+ that.compilerFlagSrc.push(...new annotation_1.Annotations(extendingModel.annotation).texts('!'));
482
482
  stepTimer.contribute([parseCompilerFlagsTimer.stop()]);
483
483
  this.importedAnnotations = true;
484
484
  }
@@ -32,18 +32,10 @@ class ModelAnnotationWalker {
32
32
  this.parseInfo = parseInfo;
33
33
  this.notes = [];
34
34
  }
35
- getLocation(cx) {
36
- return {
37
- url: this.parseInfo.sourceURL,
38
- range: this.translator.rangeFromContext(cx),
39
- };
40
- }
41
35
  enterDocAnnotations(pcx) {
42
- const allNotes = pcx.docAnnotation().map(a => ({
43
- text: (0, parse_utils_1.getAnnotationText)(a),
44
- at: this.getLocation(pcx),
45
- }));
46
- this.notes.push(...allNotes);
36
+ for (const a of pcx.docAnnotation()) {
37
+ this.notes.push((0, parse_utils_1.noteFromAnnotation)(a, this.parseInfo));
38
+ }
47
39
  }
48
40
  get annotation() {
49
41
  return { notes: this.notes };
@@ -1,6 +1,8 @@
1
1
  import type { ParserRuleContext } from 'antlr4ts';
2
2
  import type { DocAnnotationContext } from './lib/Malloy/MalloyParser';
3
3
  import { type StringContext, type ShortStringContext, type SqlStringContext, type IdContext, AnnotationContext } from './lib/Malloy/MalloyParser';
4
+ import type { Note } from '../model/malloy_types';
5
+ import type { MalloyParseInfo } from './malloy-parse-info';
4
6
  /**
5
7
  * Take the text of a matched string, including the matching quote
6
8
  * characters, and return the actual contents of the string after
@@ -35,6 +37,12 @@ export declare function unIndent(parts: (string | unknown)[]): void;
35
37
  * @returns string part and an error list.
36
38
  */
37
39
  export declare function getPlainString(cx: HasString, strictCheck?: boolean): [string | undefined, ParserRuleContext[]];
38
- type AnnotationWarn = (cx: ParserRuleContext, msg: string) => void;
39
- export declare function getAnnotationText(cx: AnnotationContext | DocAnnotationContext, warn?: AnnotationWarn): string;
40
+ /**
41
+ * Build the IR `Note` for an annotation: reads the text, dedents the body if
42
+ * it's a block, and computes the source `at` from the parse context. The
43
+ * single entry point for going from a parse-tree annotation to an IR note.
44
+ */
45
+ export declare function noteFromAnnotation(cx: AnnotationContext | DocAnnotationContext, parseInfo: MalloyParseInfo): Note;
46
+ /** Text-only reader, for callers that don't need an IR `Note`. */
47
+ export declare function getAnnotationText(cx: AnnotationContext | DocAnnotationContext): string;
40
48
  export {};
@@ -30,9 +30,11 @@ exports.idToStr = idToStr;
30
30
  exports.getOptionalId = getOptionalId;
31
31
  exports.unIndent = unIndent;
32
32
  exports.getPlainString = getPlainString;
33
+ exports.noteFromAnnotation = noteFromAnnotation;
33
34
  exports.getAnnotationText = getAnnotationText;
34
35
  const MalloyParser_1 = require("./lib/Malloy/MalloyParser");
35
36
  const malloy_tag_1 = require("@malloydata/malloy-tag");
37
+ const utils_1 = require("./utils");
36
38
  /**
37
39
  * Take the text of a matched string, including the matching quote
38
40
  * characters, and return the actual contents of the string after
@@ -182,51 +184,109 @@ function getPlainString(cx, strictCheck = false) {
182
184
  // string: shortString | sqlString; So this will never happen
183
185
  return ['', errorList];
184
186
  }
185
- function stripBlockIndent(lines, column, cx, warn) {
186
- if (column === 0) {
187
- return lines.join('');
188
- }
189
- const prefix = ' '.repeat(column);
190
- let warnedLeft = false;
191
- let warnedTab = false;
192
- return lines
193
- .map(line => {
194
- if (warn && !warnedTab && line.slice(0, column).includes('\t')) {
195
- warn(cx, 'Block annotation indentation contains tabs, use spaces');
196
- warnedTab = true;
197
- }
198
- if (line.startsWith(prefix)) {
199
- return line.slice(column);
187
+ /**
188
+ * Python `textwrap.dedent`-style: find the longest leading-whitespace prefix
189
+ * common to every non-blank body line and strip it from each line that starts
190
+ * with it. Blank (whitespace-only) lines don't constrain the prefix. Returns
191
+ * the stripped text and the number of characters removed per line — the
192
+ * latter is stored on the `Note` so payload-parser error columns can be
193
+ * mapped back to source (`source_col = indentStripped + parser_col`).
194
+ *
195
+ * Replaces an older "strip exactly opener_column spaces" rule that fired
196
+ * warnings for less-indented lines and had no clean column mapping when
197
+ * stripping was inconsistent. Common prefix is uniform per block, so column
198
+ * mapping is one number per block.
199
+ */
200
+ function dedentBlockLines(lines) {
201
+ let common;
202
+ for (const line of lines) {
203
+ const content = line.replace(/\r?\n$/, '');
204
+ if (!/\S/.test(content))
205
+ continue;
206
+ const indent = content.match(/^[ \t]*/)[0];
207
+ if (common === undefined) {
208
+ common = indent;
209
+ continue;
200
210
  }
201
- if (warn && !warnedLeft && !warnedTab && line.match(/\S/)) {
202
- warn(cx, 'Block annotation content is left of the opening #|');
203
- warnedLeft = true;
211
+ let n = 0;
212
+ while (n < common.length && n < indent.length && common[n] === indent[n]) {
213
+ n++;
204
214
  }
205
- return line;
206
- })
207
- .join('');
215
+ common = common.slice(0, n);
216
+ if (common === '')
217
+ break;
218
+ }
219
+ const prefix = common !== null && common !== void 0 ? common : '';
220
+ if (prefix === '')
221
+ return { text: lines.join(''), indentStripped: 0 };
222
+ return {
223
+ text: lines
224
+ .map(line => (line.startsWith(prefix) ? line.slice(prefix.length) : line))
225
+ .join(''),
226
+ indentStripped: prefix.length,
227
+ };
208
228
  }
209
229
  function stripTrailingNewline(s) {
210
- return s.endsWith('\n') ? s.slice(0, -1) : s;
230
+ // A trailing line ending may be CRLF or LF — strip either.
231
+ return s.replace(/\r?\n$/, '');
211
232
  }
212
- function getAnnotationText(cx, warn) {
233
+ /**
234
+ * Annotation note text is normalized to LF line endings, so a block's stored
235
+ * text and content are identical regardless of the source's CRLF/LF style.
236
+ * The lexer keeps the source `\r` in token text (it sits at line ends, after a
237
+ * line's content); this is where it is dropped.
238
+ */
239
+ function normalizeEol(s) {
240
+ return s.replace(/\r\n/g, '\n');
241
+ }
242
+ /**
243
+ * Read the text and dedent amount of an annotation from its parse tree.
244
+ * Internal — public callers want `noteFromAnnotation` or `getAnnotationText`.
245
+ */
246
+ function readAnnotation(cx) {
213
247
  if (cx instanceof MalloyParser_1.AnnotationContext) {
214
248
  const annot = cx.ANNOTATION();
215
249
  if (annot)
216
- return annot.text;
250
+ return { text: normalizeEol(annot.text), indentStripped: 0 };
217
251
  const block = cx.blockAnnotation();
218
252
  const beginToken = block.BLOCK_ANNOTATION_BEGIN();
219
253
  const textLines = block.BLOCK_ANNOTATION_TEXT().map(t => t.text);
220
- return stripTrailingNewline(beginToken.text +
221
- stripBlockIndent(textLines, beginToken.symbol.charPositionInLine, cx, warn));
254
+ const dedented = dedentBlockLines(textLines);
255
+ return {
256
+ text: normalizeEol(stripTrailingNewline(beginToken.text + dedented.text)),
257
+ indentStripped: dedented.indentStripped,
258
+ };
222
259
  }
223
260
  const doc = cx.DOC_ANNOTATION();
224
261
  if (doc)
225
- return doc.text;
262
+ return { text: normalizeEol(doc.text), indentStripped: 0 };
226
263
  const block = cx.docBlockAnnotation();
227
264
  const beginToken = block.DOC_BLOCK_ANNOTATION_BEGIN();
228
265
  const textLines = block.BLOCK_ANNOTATION_TEXT().map(t => t.text);
229
- return stripTrailingNewline(beginToken.text +
230
- stripBlockIndent(textLines, beginToken.symbol.charPositionInLine, cx, warn));
266
+ const dedented = dedentBlockLines(textLines);
267
+ return {
268
+ text: normalizeEol(stripTrailingNewline(beginToken.text + dedented.text)),
269
+ indentStripped: dedented.indentStripped,
270
+ };
271
+ }
272
+ /**
273
+ * Build the IR `Note` for an annotation: reads the text, dedents the body if
274
+ * it's a block, and computes the source `at` from the parse context. The
275
+ * single entry point for going from a parse-tree annotation to an IR note.
276
+ */
277
+ function noteFromAnnotation(cx, parseInfo) {
278
+ const { text, indentStripped } = readAnnotation(cx);
279
+ const at = {
280
+ url: parseInfo.sourceURL,
281
+ range: (0, utils_1.rangeFromContext)(parseInfo.sourceInfo, cx),
282
+ };
283
+ const note = { text, at };
284
+ if (indentStripped > 0)
285
+ note.indentStripped = indentStripped;
286
+ return note;
287
+ }
288
+ /** Text-only reader, for callers that don't need an IR `Note`. */
289
+ function getAnnotationText(cx) {
290
+ return readAnnotation(cx).text;
231
291
  }
232
292
  //# sourceMappingURL=parse-utils.js.map
@@ -1138,6 +1138,13 @@ export interface Annotation {
1138
1138
  export interface Note {
1139
1139
  text: string;
1140
1140
  at: DocumentLocation;
1141
+ /**
1142
+ * For block annotations: characters of leading whitespace removed from each
1143
+ * body line by the dedent pass. Used to map payload-parser error columns
1144
+ * back to source (`source_col = indentStripped + parser_col` for body lines).
1145
+ * Omitted for single-line notes and blocks with no common indent.
1146
+ */
1147
+ indentStripped?: number;
1141
1148
  }
1142
1149
  /** Annotations with a uuid to make it easier to stream */
1143
1150
  export interface ModelAnnotation extends Annotation {
@@ -24,7 +24,7 @@ function resolveSource(modelDef, name) {
24
24
  function checkPersistAnnotation(source) {
25
25
  if (!source.annotation)
26
26
  return { persist: false, log: [] };
27
- const { tag, log } = (0, annotation_1.annotationToTag)(source.annotation, { prefix: /^#@ / });
27
+ const { tag, log } = new annotation_1.Annotations(source.annotation).parseAsTag('@');
28
28
  return { persist: tag.has('persist'), log };
29
29
  }
30
30
  /**
@@ -206,7 +206,7 @@ class QueryStruct {
206
206
  modelCompilerFlags() {
207
207
  if (this._modelTag === undefined) {
208
208
  const annotation = this.structDef.modelAnnotation;
209
- const { tag } = (0, annotation_1.annotationToTag)(annotation, { prefix: /^##!\s*/ });
209
+ const { tag } = new annotation_1.Annotations(annotation).parseAsTag('!');
210
210
  this._modelTag = tag;
211
211
  }
212
212
  return this._modelTag;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The result of parsing an annotation's leading prefix.
3
+ *
4
+ * An annotation is `prefix sep content`. `parsePrefix` splits the captured
5
+ * annotation text at the first whitespace, strips the sigil (`#`/`##` plus an
6
+ * optional block `|`), and classifies the routing into one of three forms.
7
+ * `route` is one field; `contentIndex` and `malformation` are equally why this
8
+ * routine exists.
9
+ */
10
+ export interface ParsedPrefix {
11
+ /** The route the prefix resolves to. `''` is the MOTLY (empty) route. */
12
+ route: string;
13
+ /**
14
+ * Offset into the annotation text where the content begins;
15
+ * `text.slice(contentIndex)` is the content, with the single separator
16
+ * character excluded.
17
+ */
18
+ contentIndex: number;
19
+ /**
20
+ * Set iff the prefix is not a well-formed route. The annotation is still
21
+ * stored and reachable via the all-routes API; this only drives a warning.
22
+ * - `malformed-route`: not one of the three forms — a bare word, an unclosed
23
+ * or mismatched bracket, an empty bracketed name, or trailing junk.
24
+ * - `reserved-route`: a punct-only (sigil) routing that Malloy has not
25
+ * claimed; the punct-only namespace is reserved for Malloy's own use.
26
+ */
27
+ malformation?: 'malformed-route' | 'reserved-route';
28
+ }
29
+ /**
30
+ * Parse the leading prefix of a captured annotation.
31
+ *
32
+ * `text` is the entire annotation as the lexer captured it, marker included: a
33
+ * single line `#... content`, or a block `#|...\n<body>` whose body the lexer
34
+ * has already de-indented and whose closer it has removed. This routine never
35
+ * parses the content — it returns where the content begins.
36
+ *
37
+ * The prefix runs from the marker to the **first whitespace**; that boundary
38
+ * never moves (no bracket or quote changes it, so a route can never contain
39
+ * whitespace). After stripping the sigil (`^##?\|?`), the routing matches one
40
+ * of three forms:
41
+ *
42
+ * 1. empty -> route `''` (MOTLY, the human default)
43
+ * 2. PUNCT+ (no bracket) -> sigil route (reserved; unclaimed warns)
44
+ * 3. OPEN ... CLOSE -> route = bracketed text (opaque app route)
45
+ *
46
+ * Anything else is `malformed-route`. Bracket pairs are `()`, `<>`, `[]`, `{}`;
47
+ * the route is everything up to the matching close (first close wins, no
48
+ * nesting), taken literally — no character classification, so `#(bar-chart)`,
49
+ * `#(https://x/y)`, and `#(v1.2.3)` are all just their bracketed text.
50
+ */
51
+ export declare function parsePrefix(text: string): ParsedPrefix;
package/dist/prefix.js ADDED
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright Contributors to the Malloy project
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.parsePrefix = parsePrefix;
8
+ /**
9
+ * Punct-only (sigil) routes are reserved for Malloy's own use. This is the
10
+ * closed, enumerated set the compiler claims; any other punct-only routing
11
+ * warns `reserved-route`. The set being closed is what makes the warning
12
+ * possible — the compiler knows its own complete sigil vocabulary.
13
+ */
14
+ const CLAIMED_SIGILS = new Set(['!', '@', '"', ':']);
15
+ /** Bracket pairs an app route may use, as [open, close] — the single source. */
16
+ const BRACKET_PAIRS = [
17
+ ['(', ')'],
18
+ ['<', '>'],
19
+ ['[', ']'],
20
+ ['{', '}'],
21
+ ];
22
+ /** Two views of BRACKET_PAIRS: open->close for lookup, all halves for membership. */
23
+ const OPEN_TO_CLOSE = new Map(BRACKET_PAIRS);
24
+ const BRACKETS = new Set(BRACKET_PAIRS.flat());
25
+ /** Letter, number, or underscore — the "word" characters. */
26
+ const WORDISH = /[\p{L}\p{N}_]/u;
27
+ /**
28
+ * Parse the leading prefix of a captured annotation.
29
+ *
30
+ * `text` is the entire annotation as the lexer captured it, marker included: a
31
+ * single line `#... content`, or a block `#|...\n<body>` whose body the lexer
32
+ * has already de-indented and whose closer it has removed. This routine never
33
+ * parses the content — it returns where the content begins.
34
+ *
35
+ * The prefix runs from the marker to the **first whitespace**; that boundary
36
+ * never moves (no bracket or quote changes it, so a route can never contain
37
+ * whitespace). After stripping the sigil (`^##?\|?`), the routing matches one
38
+ * of three forms:
39
+ *
40
+ * 1. empty -> route `''` (MOTLY, the human default)
41
+ * 2. PUNCT+ (no bracket) -> sigil route (reserved; unclaimed warns)
42
+ * 3. OPEN ... CLOSE -> route = bracketed text (opaque app route)
43
+ *
44
+ * Anything else is `malformed-route`. Bracket pairs are `()`, `<>`, `[]`, `{}`;
45
+ * the route is everything up to the matching close (first close wins, no
46
+ * nesting), taken literally — no character classification, so `#(bar-chart)`,
47
+ * `#(https://x/y)`, and `#(v1.2.3)` are all just their bracketed text.
48
+ */
49
+ function parsePrefix(text) {
50
+ // 1. Split prefix from content at the first whitespace. The single separator
51
+ // character is excluded from the content.
52
+ // `\r` is in the class because Windows line endings put a `\r` right before
53
+ // the `\n` at every line end (the lexer keeps `\r` out of line *content*),
54
+ // so on a content-less prefix like `#(docs)\r\n` the `\r` must not be pulled
55
+ // into the routing. We split on it rather than mutating the text, so source
56
+ // offsets stay intact for error mapping; any line-ending bytes remain in the
57
+ // content, which is faithful to source and harmless to payload parsers.
58
+ const boundary = text.search(/[ \t\r\n]/);
59
+ const prefix = boundary === -1 ? text : text.slice(0, boundary);
60
+ const contentIndex = boundary === -1 ? text.length : boundary + 1;
61
+ // 2. Strip the sigil: one or two '#', then an optional block '|'.
62
+ const sigil = /^(#{1,2})(\|?)/.exec(prefix);
63
+ if (!sigil) {
64
+ // A captured annotation always starts with '#'; if it somehow does not,
65
+ // there is no route to speak of — likely a compiler bug upstream.
66
+ return { route: prefix, contentIndex, malformation: 'malformed-route' };
67
+ }
68
+ const routing = prefix.slice(sigil[0].length);
69
+ // Form 1: empty routing -> the MOTLY namespace.
70
+ if (routing === '') {
71
+ return { route: '', contentIndex };
72
+ }
73
+ // Form 3: opens with a bracket -> route is the text up to the matching close.
74
+ // A non-undefined close both proves routing[0] is an open bracket and gives
75
+ // us its partner in one lookup.
76
+ const close = OPEN_TO_CLOSE.get(routing[0]);
77
+ if (close !== undefined) {
78
+ const closeIdx = routing.indexOf(close, 1);
79
+ const malformed = closeIdx === -1 || // unclosed
80
+ closeIdx !== routing.length - 1 || // trailing junk after the close
81
+ closeIdx === 1; // empty bracketed text: `#()`
82
+ if (malformed) {
83
+ return { route: routing, contentIndex, malformation: 'malformed-route' };
84
+ }
85
+ return { route: routing.slice(1, closeIdx), contentIndex };
86
+ }
87
+ // Form 2: pure punctuation (no word chars, no brackets) -> sigil route.
88
+ const isPurePunct = [...routing].every(c => !WORDISH.test(c) && !BRACKETS.has(c));
89
+ if (isPurePunct) {
90
+ return {
91
+ route: routing,
92
+ contentIndex,
93
+ malformation: CLAIMED_SIGILS.has(routing) ? undefined : 'reserved-route',
94
+ };
95
+ }
96
+ // Otherwise: a bare word, or mixed text with no brackets.
97
+ return { route: routing, contentIndex, malformation: 'malformed-route' };
98
+ }
99
+ //# sourceMappingURL=prefix.js.map
@@ -1,10 +1,26 @@
1
- import type { TagParseSpec, MalloyTagParse } from './annotation';
1
+ import type { TagParseSpec, MalloyTagParse, Annotations } from './annotation';
2
2
  /**
3
3
  * Interface for objects that have Malloy tag annotations.
4
4
  * This is part of the runtime API - objects returned from the Malloy
5
5
  * runtime implement this interface to expose their tag metadata.
6
6
  */
7
7
  export interface Taggable {
8
+ /**
9
+ * Route-aware annotation access — read annotations by *route*. See
10
+ * `Annotations` for what a route is and which ones are claimed. Unlike
11
+ * `tagParse`/`getTaglines`, this sees block annotations (`#|`…`|#`).
12
+ */
13
+ readonly annotations: Annotations;
14
+ /**
15
+ * @deprecated The RegExp form cannot see block annotations (`#|`…`|#`) and
16
+ * cannot report content offsets for error mapping. Use
17
+ * `annotations.parseAsTag(route)` instead.
18
+ */
8
19
  tagParse: (spec?: TagParseSpec) => MalloyTagParse;
20
+ /**
21
+ * @deprecated The RegExp form cannot see block annotations. Use
22
+ * `annotations.texts(route)` (raw strings) or `annotations.forRoute(route)`
23
+ * (objects with offsets) instead.
24
+ */
9
25
  getTaglines: (prefix?: RegExp) => string[];
10
26
  }
@@ -70,7 +70,8 @@ async function runQueryInternal(tm, src) {
70
70
  let queryTestTag = undefined;
71
71
  try {
72
72
  query = tm.model.loadQuery(src);
73
- const queryTags = (await query.getPreparedQuery()).tagParse().tag;
73
+ const queryTags = (await query.getPreparedQuery()).annotations.parseAsTag()
74
+ .tag;
74
75
  queryTestTag = queryTags.tag('test');
75
76
  }
76
77
  catch (e) {
@@ -1,8 +1,14 @@
1
1
  import * as Malloy from '@malloydata/malloy-interfaces';
2
- import type { FieldDef, ModelDef, ResultStructMetadataDef, SourceDef } from './model';
2
+ import type { Annotation, FieldDef, ModelDef, ResultStructMetadataDef, SourceDef } from './model';
3
3
  import { Tag } from '@malloydata/malloy-tag';
4
4
  export declare function sourceDefToSourceInfo(sourceDef: SourceDef): Malloy.SourceInfo;
5
5
  export declare function modelDefToModelInfo(modelDef: ModelDef): Malloy.ModelInfo;
6
+ /**
7
+ * IR annotation → stable `Malloy.Annotation[]` shape used across `to_stable`
8
+ * and the api surfaces. The stable shape carries the raw annotation strings;
9
+ * routes are derivable at the consumer via `parsePrefix` (`./prefix.ts`).
10
+ */
11
+ export declare function toStableAnnotations(annot: Annotation | undefined): Malloy.Annotation[];
6
12
  export declare function convertFieldInfos(source: SourceDef, fields: FieldDef[]): Malloy.FieldInfo[];
7
13
  export declare function writeLiteralToTag(tag: Tag, path: (string | number)[], literal: Malloy.LiteralValue): void;
8
14
  export declare function getResultStructMetadataAnnotation(field: SourceDef, resultMetadata: ResultStructMetadataDef): Malloy.Annotation | undefined;
package/dist/to_stable.js CHANGED
@@ -41,6 +41,7 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.sourceDefToSourceInfo = sourceDefToSourceInfo;
43
43
  exports.modelDefToModelInfo = modelDefToModelInfo;
44
+ exports.toStableAnnotations = toStableAnnotations;
44
45
  exports.convertFieldInfos = convertFieldInfos;
45
46
  exports.writeLiteralToTag = writeLiteralToTag;
46
47
  exports.getResultStructMetadataAnnotation = getResultStructMetadataAnnotation;
@@ -77,7 +78,7 @@ function sourceDefToSourceInfo(sourceDef) {
77
78
  fields: convertFieldInfos(sourceDef, sourceDef.fields),
78
79
  },
79
80
  parameters,
80
- annotations: getAnnotationsFromField(sourceDef),
81
+ annotations: toStableAnnotations(sourceDef.annotation),
81
82
  };
82
83
  return sourceInfo;
83
84
  }
@@ -98,7 +99,7 @@ function modelDefToModelInfo(modelDef) {
98
99
  }
99
100
  else if (entry.type === 'query') {
100
101
  const outputStruct = (0, model_1.getResultStructDefForQuery)(modelDef, entry);
101
- const annotations = getAnnotationsFromField(entry);
102
+ const annotations = toStableAnnotations(entry.annotation);
102
103
  const resultMetadataAnnotation = outputStruct.resultMetadata
103
104
  ? getResultStructMetadataAnnotation(outputStruct, outputStruct.resultMetadata)
104
105
  : undefined;
@@ -119,7 +120,7 @@ function modelDefToModelInfo(modelDef) {
119
120
  }
120
121
  for (const query of modelDef.queryList) {
121
122
  const outputStruct = (0, model_1.getResultStructDefForQuery)(modelDef, query);
122
- const annotations = getAnnotationsFromField(query);
123
+ const annotations = toStableAnnotations(query.annotation);
123
124
  const resultMetadataAnnotation = outputStruct.resultMetadata
124
125
  ? getResultStructMetadataAnnotation(outputStruct, outputStruct.resultMetadata)
125
126
  : undefined;
@@ -173,11 +174,13 @@ function convertParameterDefaultValue(value) {
173
174
  throw new Error('Invalid parameter default value');
174
175
  }
175
176
  }
176
- function getAnnotationsFromField(field) {
177
- const taglines = (0, annotation_1.annotationToTaglines)(field.annotation);
178
- return taglines.map(tagline => ({
179
- value: tagline,
180
- }));
177
+ /**
178
+ * IR annotation → stable `Malloy.Annotation[]` shape used across `to_stable`
179
+ * and the api surfaces. The stable shape carries the raw annotation strings;
180
+ * routes are derivable at the consumer via `parsePrefix` (`./prefix.ts`).
181
+ */
182
+ function toStableAnnotations(annot) {
183
+ return new annotation_1.Annotations(annot).texts().map(value => ({ value }));
181
184
  }
182
185
  function convertFieldInfos(source, fields) {
183
186
  var _a, _b, _c;
@@ -186,10 +189,7 @@ function convertFieldInfos(source, fields) {
186
189
  const isPublic = field.accessModifier === undefined;
187
190
  if (!isPublic)
188
191
  continue;
189
- const taglines = (0, annotation_1.annotationToTaglines)(field.annotation);
190
- const rawAnnotations = taglines.map(tagline => ({
191
- value: tagline,
192
- }));
192
+ const rawAnnotations = toStableAnnotations(field.annotation);
193
193
  const annotations = rawAnnotations.length > 0 ? rawAnnotations : undefined;
194
194
  if ((0, model_1.isTurtle)(field)) {
195
195
  const outputStruct = (0, model_1.getResultStructDefForView)(source, field);
@@ -452,10 +452,7 @@ function convertRecordType(field) {
452
452
  }
453
453
  }
454
454
  if (f.annotation) {
455
- const taglines = (0, annotation_1.annotationToTaglines)(f.annotation);
456
- annotations.push(...taglines.map(tagline => ({
457
- value: tagline,
458
- })));
455
+ annotations.push(...toStableAnnotations(f.annotation));
459
456
  }
460
457
  if ((0, model_1.isAtomic)(f)) {
461
458
  return {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const MALLOY_VERSION = "0.0.396";
1
+ export declare const MALLOY_VERSION = "0.0.398";
package/dist/version.js CHANGED
@@ -2,5 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MALLOY_VERSION = void 0;
4
4
  // generated with 'generate-version-file' script; do not edit manually
5
- exports.MALLOY_VERSION = '0.0.396';
5
+ exports.MALLOY_VERSION = '0.0.398';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloy",
3
- "version": "0.0.396",
3
+ "version": "0.0.398",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -51,9 +51,9 @@
51
51
  "generate-version-file": "VERSION=$(npm pkg get version --workspaces=false | tr -d \\\")\necho \"// generated with 'generate-version-file' script; do not edit manually\\nexport const MALLOY_VERSION = '$VERSION';\" > src/version.ts"
52
52
  },
53
53
  "dependencies": {
54
- "@malloydata/malloy-filter": "0.0.396",
55
- "@malloydata/malloy-interfaces": "0.0.396",
56
- "@malloydata/malloy-tag": "0.0.396",
54
+ "@malloydata/malloy-filter": "0.0.398",
55
+ "@malloydata/malloy-interfaces": "0.0.398",
56
+ "@malloydata/malloy-tag": "0.0.398",
57
57
  "@noble/hashes": "^1.8.0",
58
58
  "antlr4ts": "^0.5.0-alpha.4",
59
59
  "assert": "^2.0.0",