jormungandr-bite/src/common/text/elements/code.js

291 lines
5.3 KiB
JavaScript
Raw Normal View History

2017-02-08 09:07:06 -07:00
/**
* Code
*/
const regexp = /```([\s\S]+?)```/;
module.exports = {
test: x => new RegExp('^' + regexp.source).test(x),
parse: text => {
const code = text.match(new RegExp('^' + regexp.source))[0];
return {
type: 'code',
content: code,
2017-02-09 03:41:17 -07:00
code: code.substr(3, code.length - 6).trim(),
codeHtml: genHtml(code.substr(3, code.length - 6).trim())
2017-02-08 09:07:06 -07:00
};
}
};
2017-02-09 03:41:17 -07:00
function escape(text) {
return text
.replace(/>/g, '>')
.replace(/</g, '&lt;');
}
// 文字数が多い順にソートします
// そうしないと、「function」という文字列が与えられたときに「func」が先にマッチしてしまう可能性があるためです
const keywords = [
'true',
'false',
'null',
'nil',
'undefined',
'var',
'const',
'let',
'mut',
'if',
'then',
'else',
'switch',
'case',
'for',
'each',
'in',
'while',
'loop',
'continue',
'break',
'do',
'goto',
'end',
'function',
'func',
'fn',
'return',
'async',
'await',
'require',
'import',
'export',
'new',
'this',
'class',
'constructor'
].sort((a, b) => b.length - a.length);
const symbols = [
'=',
'+',
'-',
'*',
'/',
'%',
2017-02-09 04:50:13 -07:00
'~',
2017-02-09 03:41:17 -07:00
'^',
'&',
'|',
'>',
'<',
2017-02-09 04:50:13 -07:00
'!',
'?'
2017-02-09 03:41:17 -07:00
];
2017-02-09 04:13:44 -07:00
// 変数宣言
const varDef = [
'var',
'const',
'let',
'mut'
];
2017-02-09 03:41:17 -07:00
const elements = [
// comment
code => {
if (code.substr(0, 2) != '//') return null;
const comment = code.match(/^\/\/(.+?)\n/)[0];
return {
html: `<span class="comment">${escape(comment)}</span>`,
next: comment.length
};
},
// string
code => {
if (!/^['"`]/.test(code)) return null;
const begin = code[0];
let str = begin;
let thisIsNotAString = false;
2017-02-09 04:23:51 -07:00
for (let i = 1; i < code.length; i++) {
2017-02-09 03:41:17 -07:00
const char = code[i];
if (char == '\\') {
2017-02-09 05:56:17 -07:00
str += char;
str += code[i + 1] || '';
2017-02-09 03:41:17 -07:00
i++;
continue;
} else if (char == begin) {
str += char;
break;
} else if (char == '\n' || i == (code.length - 1)) {
thisIsNotAString = true;
break;
} else {
str += char;
}
}
if (thisIsNotAString) {
return null;
} else {
return {
html: `<span class="string">${escape(str)}</span>`,
next: str.length
};
}
},
2017-02-09 05:47:45 -07:00
// regexp
code => {
if (code[0] != '/') return null;
let regexp = '';
let thisIsNotARegexp = false;
for (let i = 1; i < code.length; i++) {
const char = code[i];
if (char == '\\') {
2017-02-09 05:56:17 -07:00
regexp += char;
regexp += code[i + 1] || '';
2017-02-09 05:47:45 -07:00
i++;
continue;
} else if (char == '/') {
break;
} else if (char == '\n' || i == (code.length - 1)) {
thisIsNotARegexp = true;
break;
} else {
regexp += char;
}
}
if (thisIsNotARegexp) return null;
if (regexp[0] == ' ' && regexp[regexp.length - 1] == ' ') return null;
return {
html: `<span class="regexp">/${escape(regexp)}/</span>`,
next: regexp.length + 2
};
},
2017-02-09 04:13:44 -07:00
// extract vars
(code, i, source, vars) => {
const prev = source[i - 1];
if (prev && /[a-zA-Z]/.test(prev)) return null;
const match = varDef.filter(v => code.substr(0, v.length + 1) == v + ' ')[0];
if (match) {
2017-02-09 06:09:44 -07:00
const bars = code.substr(match.length + 1).match(/^[a-zA-Z0-9_\-,\s]+/);
if (bars) {
bars[0].replace(/,/g, ' ').split(' ').filter(x => x != '').forEach(bar => {
if (!keywords.some(k => k == bar)) {
vars.push(bar);
}
});
2017-02-09 04:13:44 -07:00
}
}
return null;
},
// vars
(code, i, source, vars) => {
const prev = source[i - 1];
2017-02-09 06:09:44 -07:00
// プロパティは変数と認識させないために、
// 前に . や > (PHPなどではプロパティに -> でアクセスするため) が無いかチェック
if (prev && /[a-zA-Z\.>]/.test(prev)) return null;
2017-02-09 04:13:44 -07:00
const match = vars.sort((a, b) => b.length - a.length)
.filter(v => code.substr(0, v.length) == v)[0];
if (match) {
if (/^[a-zA-Z]/.test(code.substr(match.length))) return null;
return {
html: `<span class="var">${match}</span>`,
next: match.length
};
} else {
return null;
}
},
2017-02-09 03:41:17 -07:00
// number
(code, i, source) => {
const prev = source[i - 1];
if (prev && /[a-zA-Z]/.test(prev)) return null;
2017-02-09 04:47:54 -07:00
if (!/^[\-\+]?[0-9]+/.test(code)) return null;
const match = code.match(/^[\-\+]?[0-9-]+/)[0];
2017-02-09 03:41:17 -07:00
if (match) {
return {
html: `<span class="number">${match}</span>`,
next: match.length
};
} else {
return null;
}
},
// keyword
2017-02-09 04:13:44 -07:00
(code, i, source) => {
const prev = source[i - 1];
if (prev && /[a-zA-Z]/.test(prev)) return null;
2017-02-09 03:41:17 -07:00
const match = keywords.filter(k => code.substr(0, k.length) == k)[0];
if (match) {
if (/^[a-zA-Z]/.test(code.substr(match.length))) return null;
return {
html: `<span class="keyword ${match}">${match}</span>`,
next: match.length
};
} else {
return null;
}
},
// symbol
code => {
const match = symbols.filter(s => code[0] == s)[0];
if (match) {
return {
html: `<span class="symbol">${match}</span>`,
next: 1
};
} else {
return null;
}
}
];
// specify lang is todo
function genHtml(source, lang) {
let code = source;
let html = '';
2017-02-09 04:13:44 -07:00
let vars = [];
let i = 0;
2017-02-09 03:41:17 -07:00
function push(token) {
html += token.html;
code = code.substr(token.next);
2017-02-09 04:13:44 -07:00
i += token.next;
2017-02-09 03:41:17 -07:00
}
while (code != '') {
const parsed = elements.some(el => {
2017-02-09 04:13:44 -07:00
const e = el(code, i, source, vars);
2017-02-09 03:41:17 -07:00
if (e) {
push(e);
return true;
}
});
if (!parsed) {
push({
html: escape(code[0]),
next: 1
});
}
}
return html;
}