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

383 lines
6.7 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',
2017-02-09 08:10:00 -07:00
'void',
2017-02-09 03:41:17 -07:00
'var',
'const',
'let',
'mut',
'if',
'then',
'else',
'switch',
2017-02-09 08:44:42 -07:00
'match',
2017-02-09 03:41:17 -07:00
'case',
2017-02-09 09:11:06 -07:00
'default',
2017-02-09 03:41:17 -07:00
'for',
'each',
'in',
'while',
'loop',
'continue',
'break',
'do',
'goto',
'end',
2017-02-09 09:11:06 -07:00
'throw',
'try',
'catch',
'finally',
'enum',
2017-02-09 03:41:17 -07:00
'function',
'func',
2017-02-09 09:22:59 -07:00
'fun',
2017-02-09 03:41:17 -07:00
'fn',
'return',
2017-02-09 09:20:01 -07:00
'yield',
2017-02-09 03:41:17 -07:00
'async',
'await',
'require',
2017-02-09 09:11:06 -07:00
'include',
2017-02-09 03:41:17 -07:00
'import',
'export',
2017-02-09 08:59:48 -07:00
'from',
2017-02-09 09:29:13 -07:00
'as',
2017-02-09 08:59:11 -07:00
'using',
2017-02-09 08:59:26 -07:00
'use',
2017-02-09 09:01:29 -07:00
'module',
2017-02-09 08:57:24 -07:00
'struct',
2017-02-09 09:11:06 -07:00
'union',
2017-02-09 03:41:17 -07:00
'new',
2017-02-09 09:11:06 -07:00
'delete',
2017-02-09 03:41:17 -07:00
'this',
2017-02-09 08:20:23 -07:00
'super',
'base',
2017-02-09 03:41:17 -07:00
'class',
2017-02-09 08:20:23 -07:00
'interface',
'abstract',
'static',
2017-02-09 08:10:00 -07:00
'public',
'private',
'protected',
2017-02-09 08:20:23 -07:00
'virtual',
'override',
2017-02-09 08:10:00 -07:00
'extends',
2017-02-09 08:20:23 -07:00
'implements',
2017-02-09 03:41:17 -07:00
'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
};
},
2017-02-09 07:03:16 -07:00
// block comment
code => {
const match = code.match(/^\/\*([\s\S]+?)\*\//);
if (!match) return null;
return {
html: `<span class="comment">${escape(match[0])}</span>`,
next: match[0].length
};
},
2017-02-09 03:41:17 -07:00
// 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 08:40:34 -07:00
// label
code => {
if (code[0] != '@') return null;
const label = code.match(/^@([a-zA-Z_-]+?)\n/)[0];
return {
html: `<span class="label">${label}</span>`,
next: label.length
};
},
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 09:28:47 -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;
}
},
2017-02-09 09:19:27 -07:00
// nan
(code, i, source) => {
const prev = source[i - 1];
if (prev && /[a-zA-Z]/.test(prev)) return null;
if (code.substr(0, 3) == 'NaN') {
return {
html: `<span class="nan">NaN</span>`,
next: 3
};
} else {
return null;
}
},
2017-02-09 06:47:17 -07:00
// method
code => {
const match = code.match(/^([a-zA-Z_-]+?)\(/);
if (!match) return null;
if (match[1] == '-') return null;
return {
html: `<span class="method">${match[1]}</span>`,
next: match[1].length
};
},
// property
(code, i, source) => {
const prev = source[i - 1];
if (prev != '.') return null;
2017-02-09 08:44:42 -07:00
const match = code.match(/^[a-zA-Z0-9_-]+/);
2017-02-09 06:47:17 -07:00
if (!match) return null;
return {
html: `<span class="property">${match[0]}</span>`,
next: match[0].length
};
},
2017-02-09 09:36:19 -07:00
// keyword
(code, i, source) => {
const prev = source[i - 1];
if (prev && /[a-zA-Z]/.test(prev)) return null;
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;
}
},
2017-02-09 03:41:17 -07:00
// 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;
}