From fd51047bd8a8b5d7e2e1201f6836dba2617a76c4 Mon Sep 17 00:00:00 2001 From: Ben Moon Date: Thu, 8 Jul 2021 14:06:31 +0100 Subject: [PATCH] Add Char type for ensuring priority is one char --- packages/orga/src/char.ts | 19 +++++++++++++++++++ .../src/tokenize/__tests__/headline.spec.ts | 19 ++++++++++--------- packages/orga/src/tokenize/__tests__/util.ts | 3 ++- packages/orga/src/tokenize/headline.ts | 3 ++- packages/orga/src/tokenize/types.ts | 2 ++ packages/orga/src/tokenize/util.ts | 5 +++-- packages/orga/src/types.ts | 3 ++- 7 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 packages/orga/src/char.ts diff --git a/packages/orga/src/char.ts b/packages/orga/src/char.ts new file mode 100644 index 00000000..9dfd1524 --- /dev/null +++ b/packages/orga/src/char.ts @@ -0,0 +1,19 @@ +export type Char = string & { length: 1 }; + +export const isChar = (c: string): c is Char => c.length === 1; + +export function assertChar(c: string): asserts c is Char { + if (!isChar(c)) { + throw new Error('expected string of length 1'); + } +}; + +export const char = (c: string): Char => { + assertChar(c); + return c; +} + +export const charAt = (c: string, n: number): Char | null => { + const ch = c.charAt(n); + return ch ? char(ch) : null; +} diff --git a/packages/orga/src/tokenize/__tests__/headline.spec.ts b/packages/orga/src/tokenize/__tests__/headline.spec.ts index c3261677..9e085c6f 100644 --- a/packages/orga/src/tokenize/__tests__/headline.spec.ts +++ b/packages/orga/src/tokenize/__tests__/headline.spec.ts @@ -11,6 +11,7 @@ import { tokPriority, } from './util'; +import { char, Char } from '../../char'; import { Token } from '../../types'; describe("tokenize headline", () => { @@ -25,8 +26,8 @@ describe("tokenize headline", () => { testHeadline("** a headline", 2, [tokText("a headline")]), testHeadline("***** a headline", 5, [tokText("a headline")]), testHeadline("* a 😀line", 1, [tokText("a 😀line")]), - testHeadline("* TODO [#A] a headline :tag1:tag2:", 1, [tokTodo("TODO", true), tokPriority("A"), tokText("a headline"), tokTags(["tag1", "tag2"])]), - testHeadline("* TODO [#A] a headline :tag1:123:#hash:@at:org-mode:under_score:98%:", 1, [tokTodo("TODO", true), tokPriority("A"), tokText("a headline"), tokTags(["tag1", "123", "#hash", "@at", "org-mode", "under_score", "98%"])]), + testHeadline("* TODO [#A] a headline :tag1:tag2:", 1, [tokTodo("TODO", true), tokPriority(char("A")), tokText("a headline"), tokTags(["tag1", "tag2"])]), + testHeadline("* TODO [#A] a headline :tag1:123:#hash:@at:org-mode:under_score:98%:", 1, [tokTodo("TODO", true), tokPriority(char("A")), tokText("a headline"), tokTags(["tag1", "123", "#hash", "@at", "org-mode", "under_score", "98%"])]), ]); testLexer("DONE todo keyword", ...testHeadline("* DONE heading", 1, [tokTodo("DONE", false), tokText("heading")])); @@ -55,8 +56,8 @@ describe("tokenize headline", () => { testLexer("with space is keyword", ...testHeadline("* TODO ", 1, [tokTodo("TODO", true)])); }); describe("priority cookie", () => { - testLexer("without space is cookie", ...testHeadline("* [#A]", 1, [tokPriority("A")])); - testLexer("with space is cookie", ...testHeadline("* [#A] ", 1, [tokPriority("A")])); + testLexer("without space is cookie", ...testHeadline("* [#A]", 1, [tokPriority(char("A"))])); + testLexer("with space is cookie", ...testHeadline("* [#A] ", 1, [tokPriority(char("A"))])); }); describe("tags", () => { // ambigious in v2021.07.03 spec, but Org parser does it like this (2021-07-06) @@ -72,21 +73,21 @@ describe("tokenize headline", () => { testHeadline("** DONE", 2, [tokTodo("DONE", false)]), testHeadline("*** Some e-mail", 3, [tokText("Some e-mail")]), // TODO: 'COMMENT' should be treated specially here according to the spec - testHeadline("* TODO [#A] COMMENT Title :tag:a2%:", 1, [tokTodo("TODO", true), tokPriority("A"), tokText("COMMENT Title"), tokTags(["tag", "a2%"])]), + testHeadline("* TODO [#A] COMMENT Title :tag:a2%:", 1, [tokTodo("TODO", true), tokPriority(char("A")), tokText("COMMENT Title"), tokTags(["tag", "a2%"])]), ]); describe("priority cookies", () => { testLexer('empty priority cookie is text', ...testHeadline("* [#]", 1, [tokText("[#]")])); - testLexer('uppercase letter is ok', ...testHeadline("* [#A]", 1, [tokPriority("A")])); - testLexer('lowercase letter is ok', ...testHeadline("* [#a]", 1, [tokPriority("a")])); + testLexer('uppercase letter is ok', ...testHeadline("* [#A]", 1, [tokPriority(char("A"))])); + testLexer('lowercase letter is ok', ...testHeadline("* [#a]", 1, [tokPriority(char("a"))])); // v2021.07.03 of the spec says that the priority is "a single // letter" - it is ambiguous as to whether this means 'character', // or includes digits etc., but the Org parser currently accepts // any single (ASCII) character tried (including ']') except // newline (2021-07-06) - testLexerMulti('nonletters okay', [ + testLexerMulti('nonletters okay', ([ '1', '-', '_', '?', '#', ' ', '\t', '', '\\', ']', - ].map(c => testHeadline(`* [#${c}]`, 1, [tokPriority(c)]))); + ] as Char[]).map(c => testHeadline(`* [#${c}]`, 1, [tokPriority(c)]))); testLexer('newline not okay', '* [#\n]', [tokStars(1), tokText('[#'), tokNewline(), tokText(']')]); diff --git a/packages/orga/src/tokenize/__tests__/util.ts b/packages/orga/src/tokenize/__tests__/util.ts index 5c3866d2..1ba04f82 100644 --- a/packages/orga/src/tokenize/__tests__/util.ts +++ b/packages/orga/src/tokenize/__tests__/util.ts @@ -28,6 +28,7 @@ import { Token, } from '../types'; +import { Char } from '../../char'; import tok from './tok'; import { ParseOptions } from '../../options' import * as tk from '../util'; @@ -109,7 +110,7 @@ export const tokTodo = (keyword: string, actionable: boolean, extra: Extra = {}): Priority => +export const tokPriority = (value: Char, extra: Extra = {}): Priority => tk.tokPriority(value, { _text: `[#${value}]`, ...extra }); export const tokHorizontalRule = (extra: Extra = {}): HorizontalRule => diff --git a/packages/orga/src/tokenize/headline.ts b/packages/orga/src/tokenize/headline.ts index 8ed45c0c..c5088eac 100644 --- a/packages/orga/src/tokenize/headline.ts +++ b/packages/orga/src/tokenize/headline.ts @@ -9,6 +9,7 @@ import { tokTags, tokTodo } from './util'; +import { charAt } from '../char'; interface Props { reader: Reader; @@ -48,7 +49,7 @@ export default ({ reader, todoKeywordSets }: Props): Token[] => { const priority = eat(/^\[#(.)\]/) if (!isEmpty(priority.position)) { const { value, ...rest } = priority; - buffer.push(tokPriority(value.charAt(2), rest)); + buffer.push(tokPriority(charAt(value, 2), rest)); } eat('whitespaces') diff --git a/packages/orga/src/tokenize/types.ts b/packages/orga/src/tokenize/types.ts index 842df5c2..0e579b39 100644 --- a/packages/orga/src/tokenize/types.ts +++ b/packages/orga/src/tokenize/types.ts @@ -1,4 +1,5 @@ import { Node, Literal as UnistLiteral } from 'unist'; +import { Char } from '../char'; export interface TokenI extends Node { _text?: string | undefined; @@ -34,6 +35,7 @@ export interface HorizontalRule extends TokenI { export interface Priority extends TokenLiteral { type: 'priority'; + value: Char; } export interface Tags extends TokenI { diff --git a/packages/orga/src/tokenize/util.ts b/packages/orga/src/tokenize/util.ts index 2a0e961d..2e2a3d82 100644 --- a/packages/orga/src/tokenize/util.ts +++ b/packages/orga/src/tokenize/util.ts @@ -27,6 +27,7 @@ import { Todo, Token, } from './types'; +import { Char } from '../char'; type Extra = Partial>; @@ -134,9 +135,9 @@ export const tokTodo = (keyword: string, actionable: boolean, extra: Extra = {}): Priority => ({ +export const tokPriority = (value: Char, extra: Extra = {}): Priority => ({ type: 'priority', - value: `[#${value}]`, + value, ...extra, }); diff --git a/packages/orga/src/types.ts b/packages/orga/src/types.ts index feaaa781..95110346 100644 --- a/packages/orga/src/types.ts +++ b/packages/orga/src/types.ts @@ -1,4 +1,5 @@ import { Literal as UnistLiteral, Node, Parent as UnistParent } from 'unist' +import { Char } from './char'; export { Node } from 'unist'; @@ -142,7 +143,7 @@ export interface Headline extends Parent, Child { level: number; keyword?: string; actionable: boolean; - priority?: string; + priority?: Char; content: string; tags?: string[]; // v2021.07.03 - "A headline contains directly one section