Skip to content

Instantly share code, notes, and snippets.

@nexpr
Last active June 26, 2021 11:38
Show Gist options
  • Save nexpr/2c6cc43f74b5d70f7eca9791c6a8b60c to your computer and use it in GitHub Desktop.
Save nexpr/2c6cc43f74b5d70f7eca9791c6a8b60c to your computer and use it in GitHub Desktop.

LAML

YAML 風に書ける記述法
YAML は自由度高すぎて読みづらいので構文がきつめなもの

Example


- 1
-:
	2

-:
	foo: bar
	"a:b": c:d
	nest:
		nest1:
			nest2:
				true
	
	block-str1:
		"""
		line1
		line2
		"""

	block-str2: """
		abc
		\\ \t \n
		def
		"""
	deeparray:
		-:
			-:
				1

[
  1,
  2,
  {
    "foo": "bar",
    "a:b": "c:d",
    "nest": {
      "nest1": {
        "nest2": true
      }
    },
    "block-str1": "line1\nline2",
    "block-str2": "abc\n\\\\ \\t \\n\ndef",
    "deeparray": [
      [
        1
      ]
    ]
  }
]
<!doctype html>
<meta charset="utf-8" />
<style>
* {
box-sizing: border-box;
}
body {
height: 100vh;
margin: 0;
padding: 20px;
display: flex;
flex-flow: column;
gap: 20px;
}
textarea {
flex: 1 0 0;
padding: 10px;
resize: none;
margin: 0;
tab-size: 4;
}
pre {
flex: 1 0 0;
padding: 10px;
background: #f0f0f0;
margin: 0;
}
</style>
<script type="module">
import * as laml from "./laml.js"
const update = () => {
try {
const obj = laml.parse(input.value)
output.textContent = JSON.stringify(obj, null, " ")
} catch (err) {
output.textContent = err.message
}
}
input.oninput = update
input.onkeydown = (event) => {
if (event.key === "Tab") {
event.preventDefault()
const s = input.selectionStart
const e = input.selectionEnd
input.value = input.value.slice(0, s) + "\t" + input.value.slice(e)
input.selectionStart = input.selectionEnd = s + 1
update()
}
}
</script>
<textarea id="input"></textarea>
<pre id="output"></pre>
export const parse = (source) => {
const main = (source) => {
const state = {
stack: [{ type: null, value: null }],
get current() {
return state.stack[state.stack.length - 1]
},
req_indent: 0,
unnestable: false,
push(key) {
const next_state = { type: null, value: null }
if (Array.isArray(state.current.value)) {
state.current.value.push(next_state)
} else {
state.current.value[key] = next_state
}
state.stack.push(next_state)
state.req_indent++
},
pop() {
state.req_indent--
state.stack.pop()
},
string_block: {
key: null,
lines: null,
use_nest: false,
enter(key, use_nest, obj_key) {
state.string_block.key = key
state.string_block.lines = []
state.string_block.use_nest = use_nest
if (use_nest) {
state.push(obj_key)
}
},
append(line) {
if (line === "") {
state.string_block.lines.push("")
return
}
const req_indent = "\t".repeat(state.req_indent)
if (!line.startsWith(req_indent)) {
throw new Error(`インデントレベルが不正です`)
}
const body = line.slice(state.req_indent)
if (body === state.string_block.key) {
state.current.type = "primitive"
const str = state.string_block.lines.join("\n")
state.current.value = str
state.string_block.key = null
state.string_block.lines = null
if (state.string_block.use_nest) {
state.pop()
}
} else {
state.string_block.lines.push(body)
}
},
},
}
for (const [index, line] of source.split(/\r?\n/).entries()) {
if (state.string_block.key) {
try {
state.string_block.append(line)
} catch (err) {
throw new Error(err.message + ` (${index + 1}行目)`)
}
continue
}
if (line.trim() === "") {
state.unnestable = true
continue
}
const [, indent_str, body] = line.match(/^(\t*)(.*)$/)
const indent = indent_str.length
if (indent > state.req_indent) {
throw new Error(`${index + 1}行目のインデントレベルが不正です`)
}
if (indent < state.req_indent) {
if (!state.unnestable) {
throw new Error(`${index + 1}行目のインデントが不正です\nネストの解除時は空行が必要です`)
}
if (state.current.type === null) {
throw new Error(`直前のネストに対する値が空です (${index + 1}行目)`)
}
while (indent < state.req_indent) {
state.pop()
}
}
state.unnestable = false
const parsed = parseBody(body)
const checkInvalid = (parsed) => {
if (!parsed) return
if (parsed.invalid) {
const message = {
"empty-value": "値が空です",
"extra-space": "余分な空白文字があります",
"array-nest-inline": "-: の後に文字が存在します",
}[parsed.type]
throw new Error(`${index + 1}行目が不正です:${message}`)
}
checkInvalid(parsed.value)
}
checkInvalid(parsed)
if (parsed.type === "comment") {
continue
}
if (state.current.type === "primitive") {
throw new Error(`配列・オブジェクト外では複数の値を記述できません (${index + 1}行目)`)
}
if (state.current.type === "array" && parsed.type !== "array-item" && parsed.type !== "array-nest") {
throw new Error(`配列内に不正な値があります (${index + 1}行目)`)
}
if (state.current.type === "object" && parsed.type !== "object-entry" && parsed.type !== "object-nest") {
throw new Error(`オブジェクト内に不正な値があります (${index + 1}行目)`)
}
if (["null", "boolean", "number"].includes(parsed.type)) {
state.current.type = "primitive"
state.current.value = parsed.value
continue
}
if (parsed.type === "string") {
try {
state.current.type = "primitive"
state.current.value = unescapeString(parsed.value)
} catch (err) {
throw new Error(`文字列エスケープが不正です (${index + 1}行目)`)
}
continue
}
if (parsed.type === "string-block-start") {
state.string_block.enter(parsed.value, false)
continue
}
if (parsed.type === "array-item" || parsed.type === "array-nest") {
if (state.current.type === null) {
state.current.type = "array"
state.current.value = []
}
if (parsed.type === "array-nest") {
state.push()
continue
}
const sub = parsed.value
checkInvalid(sub)
if (["null", "boolean", "number", "string"].includes(sub.type)) {
const sub_state = { type: null, value: null }
sub_state.type = "primitive"
if (sub.type === "string") {
try {
sub_state.value = unescapeString(sub.value)
} catch (err) {
throw new Error(`文字列エスケープが不正です (${index + 1}行目)`)
}
} else {
sub_state.value = sub.value
}
state.current.value.push(sub_state)
continue
}
if (sub.type === "string-block-start") {
state.string_block.enter(sub.value, true)
continue
}
throw new Error("実装エラー")
}
if (parsed.type === "object-entry" || parsed.type === "object-nest") {
if (state.current.type === null) {
state.current.type = "object"
state.current.value = {}
}
const key = parsed.value.key
if (parsed.type === "object-nest") {
state.push(key)
continue
}
const sub = parsed.value.value
checkInvalid(sub)
if (["null", "boolean", "number", "string"].includes(sub.type)) {
const sub_state = { type: null, value: null }
sub_state.type = "primitive"
if (sub.type === "string") {
try {
sub_state.value = unescapeString(sub.value)
} catch (err) {
throw new Error(`文字列エスケープが不正です (${index + 1}行目)`)
}
} else {
sub_state.value = sub.value
}
state.current.value[key] = sub_state
continue
}
if (sub.type === "string-block-start") {
state.string_block.enter(sub.value, true, key)
continue
}
throw new Error("実装エラー")
}
}
if (state.string_block.key) {
throw new Error(`文字列ブロックの終端が見つかりませんでした`)
}
if (state.current.type === null) {
throw new Error(`最後に空の値が存在します`)
}
return format(state.stack[0])
}
const parseValue = (body) => {
if (body === "") {
return { invalid: true, type: "empty-value" }
}
if (body === "true") {
return { value: true, type: "boolean" }
}
if (body === "false") {
return { value: false, type: "boolean" }
}
if (body === "null") {
return { value: null, type: "null" }
}
const is_num = body.match(/^-?\d+(\.\d+)?$/)
if (is_num) {
return { value: +body, type: "number" }
}
if (body.startsWith(`"""`)) {
return { value: body.trim(), type: "string-block-start" }
}
if (body[0] === `"` && body.slice(-1) === `"`) {
return { value: body.slice(1, -1), type: "string" }
}
return { value: body, type: "string" }
}
const unescapeString = (str) => {
return JSON.parse(`"${str}"`)
}
const parseQuote = (body) => {
if (!body.startsWith(`"`)) {
throw new Error("実装エラー")
}
let esc = false
let key = ""
for (const c of body.slice(1)) {
if (c === `"`) {
if (esc) {
key += `"`
esc = false
} else {
break
}
}
if (c === "\\") {
if (esc) {
key += "\\"
esc = false
} else {
esc = true
}
} else {
key += c
esc = false
}
}
const after = body.slice(key.length + 2)
return [key, after]
}
const parseBody = (body) => {
if (body.trimStart() !== body) {
return { invalid: true, type: "extra-space" }
}
if (body.startsWith("//")) {
return { type: "comment" }
}
if (body.startsWith("- ")) {
const sub_body = body.slice(2).trim()
return { value: parseValue(sub_body), type: "array-item" }
}
if (body.startsWith("-:")) {
if (body.trim() !== "-:") {
return { invalid: true, type: "array-nest-inline" }
}
return { type: "array-nest" }
}
if (body.includes(":")) {
if (body.startsWith(`"`)) {
// 「"foo":bar」形式を想定
const [key, after] = parseQuote(body).map((x) => x.trim())
if (after.startsWith(":")) {
const sub_body = after.slice(1).trim()
if (sub_body === "") {
return { value: { key }, type: "object-nest" }
} else {
const value = parseValue(sub_body)
return { value: { key, value }, type: "object-entry" }
}
}
// key のあとに : が来ないのは key-value ではないので通常値扱い
} else {
// 「foo:bar」形式
const pos = body.indexOf(":")
const key = body.slice(0, pos).trim()
const sub_body = body.slice(pos + 1).trim()
if (sub_body === "") {
return { value: { key }, type: "object-nest" }
} else {
const value = parseValue(sub_body)
return { value: { key, value }, type: "object-entry" }
}
}
}
return parseValue(body)
}
const format = (value) => {
if (value.type === "primitive") return value.value
if (value.type === "array") {
return value.value.map(format)
}
if (value.type === "object") {
return Object.fromEntries(Object.entries(value.value).map(([k, v]) => [k, format(v)]))
}
throw new Error("実装エラー")
}
if (typeof source !== "string" || source.trim() === "") return
return main(source)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment