Skip to content

Commit

Permalink
Add comments to query language (#2197)
Browse files Browse the repository at this point in the history
  • Loading branch information
TheMikeste1 authored Dec 30, 2023
1 parent b878580 commit 5fa107b
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 5 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 23 additions & 3 deletions src/query/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
QueryType,
SortByStep,
WhereStep,
Comment,
} from "./query";
import { Source, Sources } from "data-index/source";
import { DEFAULT_QUERY_SETTINGS } from "settings";
Expand All @@ -25,6 +26,7 @@ import { Result } from "api/result";
/** Typings for the outputs of all of the parser combinators. */
interface QueryLanguageTypes {
queryType: QueryType;
comment: Comment;

explicitNamedField: NamedField;
namedField: NamedField;
Expand Down Expand Up @@ -82,6 +84,16 @@ export const QUERY_LANGUAGE = P.createLanguage<QueryLanguageTypes>({
EXPRESSION.identifier.or(EXPRESSION.string),
(field, _as, ident) => QueryFields.named(ident, field)
),
comment: () =>
P.Parser((input, i) => {
// Parse a comment, which is a line starting with //.
let line = input.substring(i);
if (!line.startsWith("//")) return P.makeFailure(i, "Not a comment");
// The comment ends at the end of the line.
line = line.split("\n")[0];
let comment = line.substring(2).trim();
return P.makeSuccess(i + line.length, comment);
}),
namedField: q =>
P.alt<NamedField>(
q.explicitNamedField,
Expand Down Expand Up @@ -184,9 +196,9 @@ export const QUERY_LANGUAGE = P.createLanguage<QueryLanguageTypes>({
clause: q => P.alt(q.fromClause, q.whereClause, q.sortByClause, q.limitClause, q.groupByClause, q.flattenClause),
query: q =>
P.seqMap(
q.headerClause.trim(P.optWhitespace),
q.fromClause.trim(P.optWhitespace).atMost(1),
q.clause.trim(P.optWhitespace).many(),
q.headerClause.trim(optionalWhitespaceOrComment),
q.fromClause.trim(optionalWhitespaceOrComment).atMost(1),
q.clause.trim(optionalWhitespaceOrComment).many(),
(header, from, clauses) => {
return {
header,
Expand All @@ -198,6 +210,14 @@ export const QUERY_LANGUAGE = P.createLanguage<QueryLanguageTypes>({
),
});

/**
* A parser for optional whitespace or comments. This is used to exclude whitespace and comments from other parsers.
*/
const optionalWhitespaceOrComment: P.Parser<string> = P.alt(P.whitespace, QUERY_LANGUAGE.comment)
.many() // Use many() since there may be zero whitespaces or comments.
// Transform the many to a single result.
.map(arr => arr.join(""));

/**
* Attempt to parse a query from the given query text, returning a string error
* if the parse failed.
Expand Down
3 changes: 3 additions & 0 deletions src/query/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { Field } from "expression/field";
/** The supported query types (corresponding to view types). */
export type QueryType = "list" | "table" | "task" | "calendar";

/** A single-line comment. */
export type Comment = string;

/** Fields used in the query portion. */
export interface NamedField {
/** The effective name of this field. */
Expand Down
54 changes: 54 additions & 0 deletions src/test/parse/parse.query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,39 @@ test("Link Source", () => {
});
});

// <-- Comments -->
describe("Comments", () => {
test("Valid Comments", () => {
let commentWithSpace = QUERY_LANGUAGE.comment.tryParse("// This is a comment");
expect(commentWithSpace).toEqual("This is a comment");

let commentNoSpace = QUERY_LANGUAGE.comment.tryParse("//This is a comment");
expect(commentNoSpace).toEqual("This is a comment");

let empty = QUERY_LANGUAGE.comment.tryParse("//");
expect(empty).toEqual("");

let emptyWithSpace = QUERY_LANGUAGE.comment.tryParse("// ");
expect(emptyWithSpace).toEqual("");
});

test("Invalid comment in quotes", () => {
let commentSolo = QUERY_LANGUAGE.comment.parse('"// This is not a comment"');
if (!commentSolo.status) {
expect(commentSolo.expected).toEqual(["Not a comment"]);
} else {
fail("Solo comment should not have parsed");
}

let commentMiddle = QUERY_LANGUAGE.comment.parse('"The is before the comment // This is not a comment"');
if (!commentMiddle.status) {
expect(commentMiddle.expected).toEqual(["Not a comment"]);
} else {
fail("Comment in the middle of a string should not have parsed");
}
});
});

// <-- Fields -->

describe("Named Fields", () => {
Expand Down Expand Up @@ -129,6 +162,27 @@ describe("Table Queries", () => {
expect((fat.operations[1] as SortByStep).fields.length).toBe(2);
});

test("Commented Query", () => {
let commented = parseQuery(
"// This is a comment at the beginning\n" +
"TABLE (time-played + 100) as long, rating as rate, length\n" +
"// This is a comment\n" +
"// This is a second comment\n" +
"FROM #games or #gaming // This is an inline comment\n" +
"// This is a third comment\n" +
"WHERE long > 150 and rate - 10 < 40\n" +
"// This is a fourth comment\n" +
"\n" +
" // This is a comment with whitespace prior\n" +
"SORT length + 8 + 4 DESCENDING, long ASC\n" +
"// This is a comment at the end"
).orElseThrow();
expect(commented.header.type).toBe("table");
expect((commented.header as TableQuery).fields.length).toBe(3);
expect(commented.source).toEqual(Sources.binaryOp(Sources.tag("#games"), "|", Sources.tag("#gaming")));
expect((commented.operations[1] as SortByStep).fields.length).toBe(2);
});

test("WITHOUT ID", () => {
let q = parseQuery("TABLE WITHOUT ID name, value").orElseThrow();
expect(typeof q).toBe("object");
Expand Down

0 comments on commit 5fa107b

Please sign in to comment.