Skip to content

Commit 0178f1b

Browse files
authored
enhance: エスケープシーケンスの追加 (#1011)
* エスケープシーケンスの追加 * 説明の追加 * changelogの説明にバックスラッシュを追加 * コンマを挿入
1 parent 4aef45c commit 0178f1b

4 files changed

Lines changed: 128 additions & 13 deletions

File tree

src/parser/scanner.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../error.js';
2-
import { decodeUnicodeEscapeSequence } from '../utils/characters.js';
2+
import { decodeUnicodeEscapeSequence, tryDecodeSingleEscapeCharacter } from '../utils/characters.js';
33
import { CharStream } from './streams/char-stream.js';
44
import { TOKEN, TokenKind } from './token.js';
55
import { unexpectedTokenError } from './utils.js';
@@ -463,6 +463,25 @@ export class Scanner implements ITokenStream {
463463
return;
464464
}
465465

466+
private decodeEscapeSequence(): string {
467+
if (this.stream.eof) {
468+
throw new AiScriptUnexpectedEOFError(this.stream.getPos());
469+
}
470+
471+
if (this.stream.char === 'u') {
472+
const unicodeEscapeSequence = this.readUnicodeEscapeSequence();
473+
return String.fromCharCode(Number.parseInt(unicodeEscapeSequence.slice(1), 16));
474+
}
475+
476+
const decodedSingleEscapeCharacter = tryDecodeSingleEscapeCharacter(this.stream.char);
477+
if (decodedSingleEscapeCharacter != null) {
478+
this.stream.next();
479+
return decodedSingleEscapeCharacter;
480+
}
481+
482+
throw new AiScriptSyntaxError(`invalid escape character: "${this.stream.char}"`, this.stream.getPos());
483+
}
484+
466485
private readUnicodeEscapeSequence(): `u${string}` {
467486
if (this.stream.eof || (this.stream.char as string) !== 'u') {
468487
throw new AiScriptSyntaxError('character "u" expected', this.stream.getPos());
@@ -569,11 +588,7 @@ export class Scanner implements ITokenStream {
569588
break;
570589
}
571590
case 'escape': {
572-
if (this.stream.eof) {
573-
throw new AiScriptUnexpectedEOFError(pos);
574-
}
575-
value += this.stream.char;
576-
this.stream.next();
591+
value += this.decodeEscapeSequence();
577592
state = 'string';
578593
break;
579594
}
@@ -632,13 +647,7 @@ export class Scanner implements ITokenStream {
632647
break;
633648
}
634649
case 'escape': {
635-
// エスケープ対象の文字が無いままEOFに達した
636-
if (this.stream.eof) {
637-
throw new AiScriptUnexpectedEOFError(pos);
638-
}
639-
// 普通の文字として取り込み
640-
buf += this.stream.char;
641-
this.stream.next();
650+
buf += this.decodeEscapeSequence();
642651
// 通常の文字列に戻る
643652
state = 'string';
644653
break;

src/utils/characters.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,21 @@ export function decodeUnicodeEscapeSequence(string: string): string {
7272

7373
return result;
7474
}
75+
76+
export function tryDecodeSingleEscapeCharacter(s: string): string | null {
77+
switch (s) {
78+
// case 'b': return '\b';
79+
case 't': return '\t';
80+
case 'n': return '\n';
81+
// case 'v': return '\v';
82+
// case 'f': return '\f';
83+
case 'r': return '\r';
84+
case '"': return '"';
85+
case '\'': return '\'';
86+
case '\\': return '\\';
87+
case '`': return '`';
88+
case '{': return '{';
89+
case '}': return '}';
90+
default: return null;
91+
}
92+
}

test/literals.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,83 @@ describe('Template syntax', () => {
341341
});
342342
});
343343

344+
describe('Escape sequence', () => {
345+
describe('valid', () => {
346+
const cases: [string, string][] = [
347+
['\\t', '\t'], // horizontal tab
348+
['\\n', '\n'], // line feed
349+
['\\r', '\r'], // carriage return
350+
['\\"', '"'],
351+
['\\\'', '\''],
352+
['\\\\', '\\'],
353+
['\\`', '`'],
354+
['\\{', '{'],
355+
['\\}', '}'],
356+
['\\u0041', 'A'],
357+
['\\u85cd', '藍'],
358+
['\\u85CD', '藍'],
359+
['\\ud842\\udfb7', '𠮷'],
360+
['\\uD842\\uDFB7', '𠮷'],
361+
];
362+
363+
describe('double quote', () => {
364+
test.each(cases)('value of escape sequence "%s" must be "%s"', async (char, expected) => {
365+
const res = await exe(`
366+
<: "${char}"
367+
`);
368+
eq(res, STR(expected));
369+
});
370+
});
371+
372+
describe('single quote', () => {
373+
test.each(cases)('value of escape sequence "%s" must be "%s"', async (char, expected) => {
374+
const res = await exe(`
375+
<: '${char}'
376+
`);
377+
eq(res, STR(expected));
378+
});
379+
});
380+
381+
describe('template', () => {
382+
test.each(cases)('value of escape sequence "%s" must be "%s"', async (string, expected) => {
383+
const res = await exe(`
384+
<: \`${string}\`
385+
`);
386+
eq(res, STR(expected));
387+
});
388+
});
389+
});
390+
391+
describe('invalid', () => {
392+
const cases: [string][] = [
393+
['\\x'],
394+
['\\b'],
395+
['\\v'],
396+
['\\f'],
397+
];
398+
399+
describe('double quote', () => {
400+
test.each(cases)('value of escape sequence "%s" must not be allowed', async (char) => {
401+
await expect(async () => await exe(`
402+
<: "${char}"
403+
`)).rejects.toThrow(AiScriptSyntaxError);
404+
});
405+
});
406+
407+
describe('single quote', () => {
408+
test.each(cases)('value of escape sequence "%s" must not be allowed', async (char) => {
409+
await expect(async () => await exe(`
410+
<: '${char}'
411+
`)).rejects.toThrow(AiScriptSyntaxError);
412+
});
413+
});
414+
415+
describe('template', () => {
416+
test.each(cases)('value of escape sequence "%s" must not be allowed', async (string) => {
417+
await expect(async () => await exe(`
418+
<: \`${string}\`
419+
`)).rejects.toThrow(AiScriptSyntaxError);
420+
});
421+
});
422+
});
423+
});

unreleased/str_escape_sequnece.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- **Breaking change** 文字列リテラルやテンプレートリテラルにおけるエスケープシーケンスの仕様を変更しました。
2+
- 以下のエスケープシーケンスが追加されました。
3+
- `\t` - 水平タブ (U+0009)
4+
- `\n` - 改行 (U+000A)
5+
- `\r` - 復帰 (U+000D)
6+
- `\u`とそれに続く4桁の16進数の英数字 - 与えられた値を持つUTF-16コード単位として解釈されます。
7+
- `\"`, `\'`, `\\`, `` \` ``, `\{`, `\}` - それぞれ、`\`の直後の文字そのものとなります。
8+
- `\`とそれに続く文字列が上記のいずれにも一致しない場合、文法エラーが発生するようになりました。

0 commit comments

Comments
 (0)