Initial commit.
This commit is contained in:
commit
9b5ac14082
113
README.md
Normal file
113
README.md
Normal file
@ -0,0 +1,113 @@
|
||||
# Terrace Language Specification
|
||||
|
||||
The Terrace language is designed to be a minimal meta-language, capable of being both human and machine readable and writable, but doing little of its own accord other than offering a set of basic conventions that other languages built on top of Terrace can use to their own advantage.
|
||||
|
||||
## Semantics
|
||||
At its core, Terrace has only three semantic character classes:
|
||||
|
||||
1. Leading whitespace - Leading space (configurable) characters indicate the nesting level of a given section of the document.
|
||||
- No characters other than whitespace may be exist at the start of a line unless the line is at the root of the nesting hierarchy.
|
||||
2. Newlines (\n) - Newlines indicate when to start matching the next line. The \n character is matched. Carriage returns are treated literally and not used for parsing.
|
||||
3. Every other character - The first character encountered after a newline and optional sequence of indent spaces is considered the start of a line's contents. Terrace will process the line verbatim until reaching a newline character.
|
||||
|
||||
### Exception: Blank Lines
|
||||
|
||||
Blank lines will be treated as if they were indented to the same level of the previous line. This allows blocks with embedded text and documents to retain whitespace relevant to their own internal semantics even if it is stripped out by well-meaning code formatters or unintentionally ignored.
|
||||
|
||||
Example:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<th>Interpreted As</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```tce
|
||||
markdown
|
||||
# Title
|
||||
|
||||
|
||||
Dolore do do sit velit ullamco labore nisi laborum ut.
|
||||
|
||||
markdown
|
||||
# Title 2
|
||||
|
||||
Incididunt qui nulla est enim officia ad sunt excepteur consequat sunt.
|
||||
|
||||
```
|
||||
|
||||
</td>
|
||||
|
||||
<td>
|
||||
|
||||
```tce
|
||||
markdown
|
||||
-># Title
|
||||
->
|
||||
->
|
||||
->Dolore do do sit velit ullamco labore nisi laborum ut.
|
||||
->
|
||||
markdown
|
||||
-># Title 2
|
||||
->
|
||||
->Incididunt qui nulla est enim officia ad sunt excepteur consequat sunt.
|
||||
->
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Structure
|
||||
|
||||
A Terrace document consists of lines of arbitrary characters with leading whitespace indicating their nesting level relative to each other.
|
||||
|
||||
The following document contains two root-level elements, the first a line with the word "hello" with two lines of "world" nested under it, and the other line with the words "hello again" with "terrace" nested under it.
|
||||
|
||||
```tce
|
||||
hello
|
||||
world
|
||||
world
|
||||
|
||||
hello again
|
||||
terrace
|
||||
```
|
||||
|
||||
Each line may be nested arbitrarily deeper than its parent, though one level is used by convention. Terrace-based languages may introduce additional restrictions, but the core parser accepts arbitrarily deep nesting.
|
||||
|
||||
**Acceptable:**
|
||||
|
||||
```tce
|
||||
level 1
|
||||
level 2
|
||||
level 3
|
||||
level 2
|
||||
level 3
|
||||
level 4
|
||||
level 4
|
||||
level 5
|
||||
level 2
|
||||
|
||||
level 1
|
||||
level 2
|
||||
```
|
||||
|
||||
**Also Acceptable:** (even though "level 5" and "level 6" lines are nested more than one level deeper than their parent)
|
||||
|
||||
```tce
|
||||
level 1
|
||||
level 2
|
||||
level 5
|
||||
level 3
|
||||
level 6
|
||||
level 2
|
||||
|
||||
level 1
|
||||
level 2
|
||||
```
|
21
implementations/c/parser.c
Normal file
21
implementations/c/parser.c
Normal file
@ -0,0 +1,21 @@
|
||||
struct terrace_linedata_s {
|
||||
char type;
|
||||
unsigned int level;
|
||||
};
|
||||
typedef struct terrace_linedata_s terrace_linedata_t;
|
||||
|
||||
void terrace_parse_line(char* line, terrace_linedata_t *lineData) {
|
||||
char type = 0;
|
||||
unsigned int level = 0;
|
||||
|
||||
if (line[0] == '\n') {
|
||||
if (lineData->type == 1) level++;
|
||||
if (lineData->type == 0) level = lineData->level;
|
||||
} else {
|
||||
type = 1;
|
||||
while(line[level] == ' ' && level <= lineData->level + 1) ++level;
|
||||
}
|
||||
|
||||
lineData->type = type;
|
||||
lineData->level = level;
|
||||
}
|
24
implementations/js/.gitignore
vendored
Normal file
24
implementations/js/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
31
implementations/js/packages/blocks/markdown/markdown.js
Normal file
31
implementations/js/packages/blocks/markdown/markdown.js
Normal file
@ -0,0 +1,31 @@
|
||||
import {unified} from 'unified'
|
||||
import remarkParse from 'remark-parse'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import rehypeStringify from 'rehype-stringify'
|
||||
|
||||
export default function (line, lineData, doc) {
|
||||
const blockLevel = lineData.level
|
||||
|
||||
let md = ''
|
||||
|
||||
function finalize() {
|
||||
return unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeStringify)
|
||||
.process(md)
|
||||
}
|
||||
|
||||
async function markdownContents(line, lineData, doc) {
|
||||
if (lineData.level <= blockLevel) {
|
||||
const final = String(await finalize())
|
||||
page.body += final
|
||||
return doc.repeat(baseHandler)
|
||||
}
|
||||
|
||||
md += line.slice(blockLevel + 1) + '\n'
|
||||
return doc.next(markdownContents)
|
||||
}
|
||||
|
||||
return doc.next(markdownContents)
|
||||
}
|
2744
implementations/js/packages/blocks/markdown/package-lock.json
generated
Normal file
2744
implementations/js/packages/blocks/markdown/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
implementations/js/packages/blocks/markdown/package.json
Normal file
17
implementations/js/packages/blocks/markdown/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@terrace/block-markdown",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"test": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^0.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"rehype-stringify": "^9.0.3",
|
||||
"remark-parse": "^10.0.1",
|
||||
"remark-rehype": "^10.1.0",
|
||||
"unified": "^10.1.2"
|
||||
}
|
||||
}
|
1190
implementations/js/packages/core/package-lock.json
generated
Normal file
1190
implementations/js/packages/core/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
implementations/js/packages/core/package.json
Normal file
11
implementations/js/packages/core/package.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@terrace/core",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"test": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^0.15.1"
|
||||
}
|
||||
}
|
24
implementations/js/packages/core/src/parser.js
Normal file
24
implementations/js/packages/core/src/parser.js
Normal file
@ -0,0 +1,24 @@
|
||||
export function LineData() {
|
||||
return { type: 0, level: 0 }
|
||||
}
|
||||
|
||||
export function parseLine(line, lineData, indent = ' ') {
|
||||
if (typeof line !== 'string') throw new Error(`'line' must be a string`)
|
||||
if ((typeof lineData !== 'object' || !lineData) || typeof lineData.type !== 'number' || typeof lineData.level !== 'number') throw new Error(`'lineData' must be an object with 'type' and 'level' integer properties`)
|
||||
if (typeof indent !== 'string' || indent.length === 0 || indent.length > 1) throw new Error(`'indent' must be a single-character string`)
|
||||
|
||||
let type = 0
|
||||
let level = 0
|
||||
|
||||
if (!line.length) {
|
||||
if (lineData.type === 1) level += 1
|
||||
if (lineData.type === 0) level = lineData.level
|
||||
} else {
|
||||
type = 1
|
||||
while (line[level] === indent && level <= lineData.level + 1) ++level
|
||||
}
|
||||
|
||||
lineData.type = type
|
||||
lineData.level = level
|
||||
return lineData
|
||||
}
|
203
implementations/js/packages/core/src/parser.test.js
Normal file
203
implementations/js/packages/core/src/parser.test.js
Normal file
@ -0,0 +1,203 @@
|
||||
import { assert, describe, expect, it } from 'vitest'
|
||||
import { LineData, parseLine } from './parser'
|
||||
|
||||
describe(`LineData`, () => {
|
||||
it(`is an object`, () => {
|
||||
const lineData = LineData()
|
||||
expect(lineData).toBeTypeOf(`object`)
|
||||
})
|
||||
|
||||
it(`has two properties`, () => {
|
||||
const lineData = LineData()
|
||||
expect(Object.keys(lineData).length).to.equal(2)
|
||||
})
|
||||
|
||||
it(`'type' is an integer initialized to zero`, () => {
|
||||
const lineData = LineData()
|
||||
expect(lineData.level).to.equal(0)
|
||||
})
|
||||
|
||||
it(`'level' is an integer initialized to zero`, () => {
|
||||
const lineData = LineData()
|
||||
expect(lineData.type).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe(`parseLine`, () => {
|
||||
it(`Requres 'line' to be a string`, () => {
|
||||
const lineData = LineData()
|
||||
expect(() => parseLine(0, lineData)).toThrowError(`'line' must be a string`)
|
||||
expect(() => parseLine([], lineData)).toThrowError(`'line' must be a string`)
|
||||
expect(() => parseLine({}, lineData)).toThrowError(`'line' must be a string`)
|
||||
expect(() => parseLine(null, lineData)).toThrowError(`'line' must be a string`)
|
||||
expect(() => parseLine(true, lineData)).toThrowError(`'line' must be a string`)
|
||||
expect(() => parseLine(() => {}, lineData)).toThrowError(`'line' must be a string`)
|
||||
})
|
||||
|
||||
it(`Requres 'lineData' to be an object with a numeric level and type property`, () => {
|
||||
const lineData = LineData()
|
||||
expect(() => parseLine(``, 0)).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`)
|
||||
expect(() => parseLine(``, [])).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`)
|
||||
expect(() => parseLine(``, {})).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`)
|
||||
expect(() => parseLine(``, null)).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`)
|
||||
expect(() => parseLine(``, true)).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`)
|
||||
expect(() => parseLine(``, () => {})).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`)
|
||||
expect(() => parseLine(``, { level: '', type: 0 })).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`)
|
||||
expect(() => parseLine(``, { level: 0, type: null })).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`)
|
||||
})
|
||||
|
||||
it(`Requres 'indent' to be a single-character string`, () => {
|
||||
const lineData = LineData()
|
||||
expect(() => parseLine(``, lineData, 0)).toThrowError(`'indent' must be a single-character string`)
|
||||
expect(() => parseLine(``, lineData, [])).toThrowError(`'indent' must be a single-character string`)
|
||||
expect(() => parseLine(``, lineData, {})).toThrowError(`'indent' must be a single-character string`)
|
||||
expect(() => parseLine(``, lineData, null)).toThrowError(`'indent' must be a single-character string`)
|
||||
expect(() => parseLine(``, lineData, true)).toThrowError(`'indent' must be a single-character string`)
|
||||
expect(() => parseLine(``, lineData, () => {})).toThrowError(`'indent' must be a single-character string`)
|
||||
expect(() => parseLine(``, lineData, ` `)).toThrowError(`'indent' must be a single-character string`)
|
||||
})
|
||||
|
||||
it(`Outputs { type: 0, level: 0} for a blank line at indent level 0`, () => {
|
||||
const line = ``
|
||||
const lineData = LineData()
|
||||
parseLine(line, lineData)
|
||||
expect(lineData.type).to.equal(0)
|
||||
expect(lineData.level).to.equal(0)
|
||||
})
|
||||
|
||||
it(`Outputs { type: 1, level: 1} for line with a single space at indent level 1`, () => {
|
||||
const line = ` `
|
||||
const lineData = LineData()
|
||||
parseLine(line, lineData)
|
||||
expect(lineData.type).to.equal(1)
|
||||
expect(lineData.level).to.equal(1)
|
||||
})
|
||||
|
||||
it(`Outputs { type: 1, level: 2} for line with two spaces`, () => {
|
||||
const line = ` `
|
||||
const lineData = LineData()
|
||||
parseLine(line, lineData)
|
||||
expect(lineData.type).to.equal(1)
|
||||
expect(lineData.level).to.equal(2)
|
||||
})
|
||||
|
||||
it(`Outputs { type: 1, level: 0} for a normal line at indent level 0`, () => {
|
||||
const line = `line 1`
|
||||
const lineData = LineData()
|
||||
parseLine(line, lineData)
|
||||
expect(lineData.type).to.equal(1)
|
||||
expect(lineData.level).to.equal(0)
|
||||
})
|
||||
|
||||
it(`Outputs { type: 1, level: 1} for a normal line at indent level 1`, () => {
|
||||
const line = ` line 1`
|
||||
const lineData = LineData()
|
||||
parseLine(line, lineData)
|
||||
expect(lineData.type).to.equal(1)
|
||||
expect(lineData.level).to.equal(1)
|
||||
})
|
||||
|
||||
it(`Outputs { type: 1, level: 1} for a normal line at indent level 1`, () => {
|
||||
const line = ` line 1`
|
||||
const lineData = LineData()
|
||||
parseLine(line, lineData)
|
||||
expect(lineData.type).to.equal(1)
|
||||
expect(lineData.level).to.equal(2)
|
||||
})
|
||||
|
||||
it(`Outputs { type: 1, level: 1} for a normal line at indent level 1 indented with tabs`, () => {
|
||||
const line = `\tline 1`
|
||||
const lineData = LineData()
|
||||
parseLine(line, lineData, `\t`)
|
||||
expect(lineData.type).to.equal(1)
|
||||
expect(lineData.level).to.equal(1)
|
||||
})
|
||||
|
||||
it(`Outputs { type: 1, level: 2} for a normal line at indent level 1 indented with tabs`, () => {
|
||||
const line = `\t\tline 1`
|
||||
const lineData = LineData()
|
||||
parseLine(line, lineData, `\t`)
|
||||
expect(lineData.type).to.equal(1)
|
||||
expect(lineData.level).to.equal(2)
|
||||
})
|
||||
|
||||
it(`Nests a normal line under a preceding normal line`, () => {
|
||||
const lines = [
|
||||
'line 1',
|
||||
' line 2'
|
||||
]
|
||||
|
||||
const lineData = LineData()
|
||||
const results = lines.map(line => {
|
||||
parseLine(line, lineData)
|
||||
return {...lineData}
|
||||
})
|
||||
|
||||
expect(results).to.deep.equal([
|
||||
{ type: 1, level: 0 },
|
||||
{ type: 1, level: 1 }
|
||||
])
|
||||
})
|
||||
|
||||
it(`Nests multiple normal line under a preceding normal line`, () => {
|
||||
const lines = [
|
||||
'line 1',
|
||||
' line 2',
|
||||
' line 3',
|
||||
' line 4',
|
||||
]
|
||||
|
||||
const lineData = LineData()
|
||||
const results = lines.map(line => {
|
||||
parseLine(line, lineData)
|
||||
return {...lineData}
|
||||
})
|
||||
|
||||
expect(results).to.deep.equal([
|
||||
{ type: 1, level: 0 },
|
||||
{ type: 1, level: 1 },
|
||||
{ type: 1, level: 1 },
|
||||
{ type: 1, level: 1 }
|
||||
])
|
||||
})
|
||||
|
||||
it(`Nests an empty line under a preceding normal line`, () => {
|
||||
const lines = [
|
||||
'line 1',
|
||||
''
|
||||
]
|
||||
|
||||
const lineData = LineData()
|
||||
const results = lines.map(line => {
|
||||
parseLine(line, lineData)
|
||||
return {...lineData}
|
||||
})
|
||||
|
||||
expect(results).to.deep.equal([
|
||||
{ type: 1, level: 0 },
|
||||
{ type: 0, level: 1 }
|
||||
])
|
||||
})
|
||||
|
||||
it(`Nests multiple empty lines under a preceding normal line`, () => {
|
||||
const lines = [
|
||||
'line 1',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
]
|
||||
|
||||
const lineData = LineData()
|
||||
const results = lines.map(line => {
|
||||
parseLine(line, lineData)
|
||||
return {...lineData}
|
||||
})
|
||||
|
||||
expect(results).to.deep.equal([
|
||||
{ type: 1, level: 0 },
|
||||
{ type: 0, level: 1 },
|
||||
{ type: 0, level: 1 },
|
||||
{ type: 0, level: 1 }
|
||||
])
|
||||
})
|
||||
})
|
23
implementations/js/packages/core/src/terrace.js
Normal file
23
implementations/js/packages/core/src/terrace.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { LineData, parseLine } from './parser';
|
||||
|
||||
|
||||
export function document(nextFn, indent = ' ') {
|
||||
let line = null
|
||||
const lineData = LineData()
|
||||
|
||||
async function next(handler) {
|
||||
line = await nextFn()
|
||||
parseLine(line, lineData, indent)
|
||||
return handler(line, lineData, { next, repeat, end })
|
||||
}
|
||||
|
||||
function repeat(handler) {
|
||||
return handler(line, lineData, { next, repeat, end })
|
||||
}
|
||||
|
||||
function end() {
|
||||
return
|
||||
}
|
||||
|
||||
return { next, repeat, end }
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user