zenwerk
https://gyazo.com/461816c9ddf72d926b31e9f1cf873c44
$ 1 + 1
$ ls -a
パーサーコード
code:script.js
const parser = (function() {
"use strict";
function peg$subclass(child, parent) {
function ctor() { this.constructor = child; }
ctor.prototype = parent.prototype;
child.prototype = new ctor();
}
function peg$SyntaxError(message, expected, found, location) {
this.message = message;
this.expected = expected;
this.found = found;
this.location = location;
this.name = "SyntaxError";
if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, peg$SyntaxError);
}
}
peg$subclass(peg$SyntaxError, Error);
peg$SyntaxError.buildMessage = function(expected, found) {
var DESCRIBE_EXPECTATION_FNS = {
literal: function(expectation) {
return "\"" + literalEscape(expectation.text) + "\"";
},
"class": function(expectation) {
var escapedParts = "",
i;
for (i = 0; i < expectation.parts.length; i++) {
escapedParts += expectation.partsi instanceof Array ? classEscape(expectation.partsi0) + "-" + classEscape(expectation.partsi1) : classEscape(expectation.partsi); }
return "+ (expectation.inverted ? "^" : "") + escapedParts + "";
},
any: function(expectation) {
return "any character";
},
end: function(expectation) {
return "end of input";
},
other: function(expectation) {
return expectation.description;
}
};
function hex(ch) {
return ch.charCodeAt(0).toString(16).toUpperCase();
}
function literalEscape(s) {
return s
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\0/g, '\\0')
.replace(/\t/g, '\\t')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\x00-\x0F/g, function(ch) { return '\\x0' + hex(ch); }) }
function classEscape(s) {
return s
.replace(/\\/g, '\\\\')
.replace(/\]/g, '\\]')
.replace(/\^/g, '\\^')
.replace(/-/g, '\\-')
.replace(/\0/g, '\\0')
.replace(/\t/g, '\\t')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\x00-\x0F/g, function(ch) { return '\\x0' + hex(ch); }) }
function describeExpectation(expectation) {
}
function describeExpected(expected) {
var descriptions = new Array(expected.length),
i, j;
for (i = 0; i < expected.length; i++) {
descriptionsi = describeExpectation(expectedi); }
descriptions.sort();
if (descriptions.length > 0) {
for (i = 1, j = 1; i < descriptions.length; i++) {
if (descriptionsi - 1 !== descriptionsi) { descriptionsj = descriptionsi; j++;
}
}
descriptions.length = j;
}
switch (descriptions.length) {
case 1:
case 2:
return descriptions0 + " or " + descriptions1; default:
return descriptions.slice(0, -1).join(", ")
+ ", or "
}
}
function describeFound(found) {
return found ? "\"" + literalEscape(found) + "\"" : "end of input";
}
return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found.";
};
function peg$parse(input, options) {
options = options !== void 0 ? options : {};
var peg$FAILED = {},
peg$startRuleIndices = { start: 0 },
peg$startRuleIndex = 0,
peg$consts = [
function(lines) {
return {lines};
},
function(indent, contents, blankline) {
if (blankline) { result.push(blankline); }
return result;
},
function(indent, content) {
if (content) { result.push({type: 'contents', contents: content }); } return result;
},
function(blankline) {
return [
{type: 'indent', level: 0},
];
},
function(indent) {
return {type: 'indent', level: indent.length};
},
function(contents) {
return {type: 'contents', contents: contents};
},
function(text) { return {type: 'text', text: text}; },
"`",
peg$literalExpectation("`", false),
function(text) { return {type: 'backquote', text: text}; },
"[[",
peg$literalExpectation("[[", false),
"]]",
peg$literalExpectation("]]", false),
function(contents) {
return {type: 'decoration', bold: 1, italic: false, strikethrough: false, underline: false, contents:contents};
},
"[",
peg$literalExpectation("[", false),
peg$classExpectation("*", "/", "_", false, false),
"]",
peg$literalExpectation("]", false),
function(deco, contents) {
let bold = 0;
let italic = false;
let strikethrough = false;
let underline = false;
deco.forEach(ch => {
if (ch === '*') { bold += 1; }
if (ch === '/') { italic = true; }
if (ch === '-') { strikethrough = true; }
if (ch === '_') { underline = true; }
})
return {type: 'decoration', bold, italic, strikethrough, underline, contents};
},
function(link) { return link; },
function(url) { return {type: 'link', url: url, text: url}; },
function(url, text) { return {type: 'link', url: url, text: text}; },
function(text, url) { return {type: 'link', url: url, text: text.join(' ')}; },
function(url) { return {type: 'link', url: url, text: ''}; },
function(text) { return {type: 'link', url:text, text: text, internal: true}; },
function(text) { return text; },
"#",
peg$literalExpectation("#", false),
function(text) {
return {type: 'hash', text:text};
},
"[$",
peg$literalExpectation("[$", false),
function(text) {
return {type: 'tex', text: text};
},
">",
peg$literalExpectation(">", false),
function(text) {
return {type: 'quote', text: text};
},
"$",
peg$literalExpectation("$", false),
function(text) {
return {type: 'shell', text: text};
},
"code:",
peg$literalExpectation("code:", false),
function(text) {
return {type: 'codeblock', name: text};
},
"table:",
peg$literalExpectation("table:", false),
function(text) {
return {type: 'table', name: text};
},
"http",
peg$literalExpectation("http", false),
"s",
peg$literalExpectation("s", false),
"://",
peg$literalExpectation("://", false),
function(secure, url) {
return 'http' + (secure ? 's': '') + '://' + url;
},
function(t) {
return t;
},
peg$classExpectation("\n", false, false), function() { return {type:'blank'}; },
peg$classExpectation(["\n", "\r", """], true, false),
peg$classExpectation(", "\u3000", "\n", "\t", "\r", "", "", true, false), peg$classExpectation(["`", "\n", "\r", """], true, false),
peg$classExpectation(", "\u3000", "\t", false, false)
],
peg$bytecode = [
peg$decode(";!"),
peg$decode("%$;\"0#*;\"&/' 8!: !! )"),
peg$decode("%;#/A#;%/8$;5.\" &\"/*$8#:!##\"! )(#'#(\"'#&'#.S &%;$/7#;&.\" &\"/)$8\":\"\"\"! )(\"'#&'#./ &%;5/' 8!:#!! )"),
peg$decode("%;:/' 8!:$!! )"),
peg$decode("%;;/' 8!:$!! )"),
peg$decode("%$;&/�#*;&&&&#/' 8!:%!! )"),
peg$decode(";'.e &;(._ &;).Y &;-.S &;0.M &;..G &;+.A &;/.; &;1.5 &;2./ &%;8/' 8!:&!! )"),
peg$decode("%2'\"\"6'7(/`#%$4)\"\"5!7*/,#0)*4)\"\"5!7*&&&#/\"!&,)/7$2'\"\"6'7(/($8#:+#!!)(#'#(\"'#&'#"),
peg$decode("%2,\"\"6,7-/_#;:/V$$;*/�#*;*&&&#/@$;:/7$2.\"\"6.7//($8%:0%!\")(%'#($'#(#'#(\"'#&'#"),
peg$decode("%21\"\"6172/\x82#$43\"\"5!74/,#0)*43\"\"5!74&&&#/`$;;/W$$;*/�#*;*&&&#/A$;:/8$25\"\"6576/)$8&:7&\"$\")(&'#(%'#($'#(#'#(\"'#&'#"),
peg$decode("%;+/1#;:/($8\":8\"!!)(\"'#&'#.; &%;7/1#;:/($8\":&\"!!)(\"'#&'#"),
peg$decode("%;3/' 8!:9!! ).\u015B &%21\"\"6172/\\#;:/S$;3/J$;;/A$;6/8$25\"\"6576/)$8&::&\"#!)(&'#(%'#($'#(#'#(\"'#&'#.\u010C &%21\"\"6172/r#;:/i$$;,/�#*;,&&&#/S$;:/J$;3/A$;:/8$25\"\"6576/)$8':;'\"$\")(''#(&'#(%'#($'#(#'#(\"'#&'#.\xA7 &%21\"\"6172/R#;:/I$;3/@$;:/7$25\"\"6576/($8%:<%!\")(%'#($'#(#'#(\"'#&'#.b &%21\"\"6172/R#;:/I$;6/@$;:/7$25\"\"6576/($8%:=%!\")(%'#($'#(#'#(\"'#&'#"),
peg$decode("%%;7/,#;;/#$+\")(\"'#&'#/' 8!:>!! )"),
peg$decode("%2?\"\"6?7@/1#;7/($8\":A\"! )(\"'#&'#"),
peg$decode("%2B\"\"6B7C/I#;;/@$;6/7$25\"\"6576/($8$:D$!!)($'#(#'#(\"'#&'#"),
peg$decode("%2E\"\"6E7F/:#;:/1$;7/($8#:G#! )(#'#(\"'#&'#"),
peg$decode("%2H\"\"6H7I/T#;;/K$%$4J\"\"5!7K0)*4J\"\"5!7K&/\"!&,)/($8#:L#! )(#'#(\"'#&'#"),
peg$decode("%2M\"\"6M7N/1#;7/($8\":O\"! )(\"'#&'#"),
peg$decode("%2P\"\"6P7Q/1#;7/($8\":R\"! )(\"'#&'#"),
peg$decode("%2S\"\"6S7T/U#2U\"\"6U7V.\" &\"/A$2W\"\"6W7X/2$;7/)$8$:Y$\"\" )($'#(#'#(\"'#&'#"),
peg$decode("%;7/' 8!:Z!! )"),
peg$decode("%$4^\"\"5!7_/,#0)*4^\"\"5!7_&&&#/\"!&,)"),
peg$decode("%$4\"\"5!7a/,#0)*4\"\"5!7a&&&#/\"!&,)"),
peg$decode("%$4b\"\"5!7c/,#0)*4b\"\"5!7c&&&#/\"!&,)"),
peg$decode("4d\"\"5!7e"),
peg$decode("$;90#*;9&"),
peg$decode("$;9/�#*;9&&&#")
],
peg$currPos = 0,
peg$savedPos = 0,
peg$posDetailsCache = line: 1, column: 1 },
peg$maxFailPos = 0,
peg$maxFailExpected = [],
peg$silentFails = 0,
peg$result;
if ("startRule" in options) {
if (!(options.startRule in peg$startRuleIndices)) {
throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
}
}
function text() {
return input.substring(peg$savedPos, peg$currPos);
}
function location() {
return peg$computeLocation(peg$savedPos, peg$currPos);
}
function expected(description, location) {
location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos)
throw peg$buildStructuredError(
input.substring(peg$savedPos, peg$currPos),
location
);
}
function error(message, location) {
location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos)
throw peg$buildSimpleError(message, location);
}
function peg$literalExpectation(text, ignoreCase) {
return { type: "literal", text: text, ignoreCase: ignoreCase };
}
function peg$classExpectation(parts, inverted, ignoreCase) {
return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase };
}
function peg$anyExpectation() {
return { type: "any" };
}
function peg$endExpectation() {
return { type: "end" };
}
function peg$otherExpectation(description) {
return { type: "other", description: description };
}
function peg$computePosDetails(pos) {
var details = peg$posDetailsCachepos, p; if (details) {
return details;
} else {
p = pos - 1;
while (!peg$posDetailsCachep) { p--;
}
details = peg$posDetailsCachep; details = {
line: details.line,
column: details.column
};
while (p < pos) {
if (input.charCodeAt(p) === 10) {
details.line++;
details.column = 1;
} else {
details.column++;
}
p++;
}
peg$posDetailsCachepos = details; return details;
}
}
function peg$computeLocation(startPos, endPos) {
var startPosDetails = peg$computePosDetails(startPos),
endPosDetails = peg$computePosDetails(endPos);
return {
start: {
offset: startPos,
line: startPosDetails.line,
column: startPosDetails.column
},
end: {
offset: endPos,
line: endPosDetails.line,
column: endPosDetails.column
}
};
}
function peg$fail(expected) {
if (peg$currPos < peg$maxFailPos) { return; }
if (peg$currPos > peg$maxFailPos) {
peg$maxFailPos = peg$currPos;
peg$maxFailExpected = [];
}
peg$maxFailExpected.push(expected);
}
function peg$buildSimpleError(message, location) {
return new peg$SyntaxError(message, null, null, location);
}
function peg$buildStructuredError(expected, found, location) {
return new peg$SyntaxError(
peg$SyntaxError.buildMessage(expected, found),
expected,
found,
location
);
}
function peg$decode(s) {
var bc = new Array(s.length), i;
for (i = 0; i < s.length; i++) {
bci = s.charCodeAt(i) - 32; }
return bc;
}
function peg$parseRule(index) {
var bc = peg$bytecodeindex, ip = 0,
ips = [],
end = bc.length,
ends = [],
stack = [],
params, i;
while (true) {
while (ip < end) {
case 0:
stack.push(peg$consts[bcip + 1]); ip += 2;
break;
case 1:
stack.push(void 0);
ip++;
break;
case 2:
stack.push(null);
ip++;
break;
case 3:
stack.push(peg$FAILED);
ip++;
break;
case 4:
stack.push([]);
ip++;
break;
case 5:
stack.push(peg$currPos);
ip++;
break;
case 6:
stack.pop();
ip++;
break;
case 7:
peg$currPos = stack.pop();
ip++;
break;
case 8:
ip += 2;
break;
case 9:
stack.splice(-2, 1);
ip++;
break;
case 10:
ip++;
break;
case 11:
stack.push(stack.splice(stack.length - bcip + 1, bcip + 1)); ip += 2;
break;
case 12:
stack.push(input.substring(stack.pop(), peg$currPos));
ip++;
break;
case 13:
ends.push(end);
ip += 3;
} else {
}
break;
case 14:
ends.push(end);
ip += 3;
} else {
}
break;
case 15:
ends.push(end);
ip += 3;
} else {
}
break;
case 16:
ends.push(end);
ips.push(ip);
ip += 2;
} else {
}
break;
case 17:
ends.push(end);
if (input.length > peg$currPos) {
ip += 3;
} else {
}
break;
case 18:
ends.push(end);
if (input.substr(peg$currPos, peg$consts[bcip + 1].length) === peg$consts[bcip + 1]) { ip += 4;
} else {
}
break;
case 19:
ends.push(end);
if (input.substr(peg$currPos, peg$consts[bcip + 1].length).toLowerCase() === peg$consts[bcip + 1]) { ip += 4;
} else {
}
break;
case 20:
ends.push(end);
if (peg$consts[bcip + 1].test(input.charAt(peg$currPos))) { ip += 4;
} else {
}
break;
case 21:
stack.push(input.substr(peg$currPos, bcip + 1)); ip += 2;
break;
case 22:
stack.push(peg$consts[bcip + 1]); peg$currPos += peg$consts[bcip + 1].length; ip += 2;
break;
case 23:
stack.push(peg$FAILED);
if (peg$silentFails === 0) {
peg$fail(peg$consts[bcip + 1]); }
ip += 2;
break;
case 24:
peg$savedPos = stack[stack.length - 1 - bcip + 1]; ip += 2;
break;
case 25:
peg$savedPos = peg$currPos;
ip++;
break;
case 26:
params = bc.slice(ip + 4, ip + 4 + bcip + 3); for (i = 0; i < bcip + 3; i++) { paramsi = stack[stack.length - 1 - paramsi]; }
stack.splice(
peg$consts[bcip + 1].apply(null, params) );
break;
case 27:
stack.push(peg$parseRule(bcip + 1)); ip += 2;
break;
case 28:
peg$silentFails++;
ip++;
break;
case 29:
peg$silentFails--;
ip++;
break;
default:
throw new Error("Invalid opcode: " + bcip + "."); }
}
if (ends.length > 0) {
end = ends.pop();
ip = ips.pop();
} else {
break;
}
}
}
// TODO: 現在のインデントレベルを保持して codeblock の範囲を判断するコード
var indentLevel = 0;
peg$result = peg$parseRule(peg$startRuleIndex);
if (peg$result !== peg$FAILED && peg$currPos === input.length) {
return peg$result;
} else {
if (peg$result !== peg$FAILED && peg$currPos < input.length) {
peg$fail(peg$endExpectation());
}
throw peg$buildStructuredError(
peg$maxFailExpected,
peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,
peg$maxFailPos < input.length
? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)
: peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
);
}
}
return {
SyntaxError: peg$SyntaxError,
parse: peg$parse
};
})();
code:script.js
//import parser from './parser';
// import './style.css';
const SCRAPBOX_DOMAIN = 'scrapbox.io';
const REFERENCE_PROJECT = 'ref';
// TODO: グローバル変数での状態管理をやめる
let PROJECT_NAME = '';
let CURRENT_INDENT_LEVEL = 0;
let IN_CODEBLOCK = false; // 現在コードブロックの中か?
let DO_CODEBLOCK_ESCAPE_PROCESS = false; // コードブロックの閉じタグ処理を行うためのフラグ
// let EMPTY_LINKS = [];
const INDENT = 'indent';
const CONTENTS = 'contents';
const LINK = 'link';
const DECORATION = 'decoration';
const BACKQUOTE = 'backquote';
const TEXT = 'text';
const HASH = 'hash';
const QUOTE = 'quote';
const TEX = 'tex';
const SHELL = 'shell';
const CODEBLOCK = 'codeblock';
const TABLE = 'table';
const BLANK = 'blank';
// Scrapbox のプロジェクト名をURLから探して返す
const getProjectName = () => {
const r = window.location.href.match(/scrapbox\.io\/(^/.*)/) || window.location.href.match(/localhost:\d+\/(^/.*)/); if (r && r.length >= 2) {
return encodeURIComponent(r1); }
return 'zenwerk';
};
const getScrapboxUrl = url => (
);
const getDomain = url => {
const a = document.createElement('a');
a.href = url;
return a.hostname;
};
const getPathname = url => {
const a = document.createElement('a');
a.href = url;
return a.pathname;
};
const copyToClipboard = text => {
const textarea = document.createElement("textarea");
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("Copy");
textarea.parentElement.removeChild(textarea);
};
// テキスト一文字ずつ <span> で囲む処理
const spans = (txt, index = 0) => {
let body = '';
for (let k = 0; k < txt.length; k++) {
body += <span class="c-${index}">${txt[k]}</span>;
index++;
}
return body;
};
const encodeHref = (url, startsWithHttp) => {
const tt = url.match(/scrapbox\.io\/(^/+)\/(.+)/); if (startsWithHttp || tt === null) {
url = url.replace(/</gi, '%3C').replace(/>/gi, '%3E').replace(/;/gi, '%3B');
return url;
}
const pageRowNum = pageName.match(/#.{24,32}$/);
if (pageRowNum) {
// 行リンク
pageName = encodeURIComponent(pageName.split(n)0) + n; } else {
pageName = encodeURIComponent(pageName);
}
return url.replace(tt2, pageName); };
// 画像になる可能性があるものをimgタグにして返す
const makeImageTag = (href) => {
href = href.trim();
let imgTag = '';
let isImg = true;
if (href.match(/\.icon\**\d*$/gi)) {
// scrapbox の icon 対応
let iconName = href.split('.icon')0; if (iconName.charAt(0) !== '/') {
iconName = '/' + PROJECT_NAME + '/' + iconName;
}
const tokens = href.split('*');
let times = 1;
if (tokens.length === 2) {
}
for (let i = 0; i < times; i++) {
imgTag += <img class="popup-tiny-icon" src="https://scrapbox.io/api/pages${iconName}/icon">;
}
} else if (href.endsWith('.jpg') || href.endsWith('.png') || href.endsWith('.gif')) {
imgTag = <img class="popup-small-img" src="${href}">;
} else if (href.match(/^https{0,1}:\/\/gyazo.com\/.{24,32}$/)) {
imgTag = <img class="popup-small-img" src="${href}/raw">;
} else {
imgTag = href;
isImg = false;
}
};
// Shell記法
const makeShellTag = (elm) => (
`<code class="cli">
<span class="prefix"><span class="c-0">$</span></span>
<span class="c-1"> </span>
<span class="command">
${spans(elm.text, 2)}
</span>
</code>`
);
// 引用符で挟まれたコード
const makeBackQuoteTag = elm => (
<code class="code"><span class="popup-backquote">${spans(elm.text)}</span></code>
);
// 引用タグ
const makeQuoteTag = elm => (
<span class="popup-quote">${spans(elm.text)}</span>
);
// 装飾処理など
const makeDecorationTag = (elm) => {
let html = '';
elm.contents.forEach(content => {
html += decorate(content);
});
if (elm.underline) {
html = <span class="popup-underline">${html}</span>;
}
if (elm.italic) {
html = <i>${html}</i>;
}
if (elm.strikethrough) {
html = <s>${html}</s>;
}
if (elm.bold) {
html = <b>${html}</b>;
}
return html;
};
const makeLinkTag = (elm) => {
let body = elm.text;
let href = elm.url;
if (isImg) {
body = imgTag;
} else {
body = spans(body);
}
return <a href="${encodeHref(href, true)}" class="popup-ref-link" target="_blank">${body}</a>;
};
const makeInternalLinkTag = (elm) => {
// let classEmptyLink = '';
// if (EMPTY_LINKS.indexOf(body) !== -1) classEmptyLink = 'empty-page-link';
const target = (PROJECT_NAME !== getProjectName()) ? '_blank' : '_self';
let href = '';
if (elm.text.indexOf('http') !== -1) {
href = getPathname(elm.url)
} else if (elm.text.charAt(0) === '/') {
href = elm.text;
}
return <a href="${encodeHref(getScrapboxUrl(href), false)}" class="page-link" target="${target}">${spans(elm.text)}</a>;
};
const makeHashLinkTag = (elm) => {
const keyword = encodeURIComponent(elm.text);
const target = (PROJECT_NAME !== getProjectName()) ? '_blank' : '_self';
return <a href="/${PROJECT_NAME}/${keyword}" class="page-link" target="${target}">${spans(elm.text)}</a>;
};
const makeCodeStartTag = (elm, state) => (
`<span class="popup-code-block">
<a href="/api/code/${state.projectName}/${state.title}/${elm.name}" target="_blank"><span class="popup-code-block-start">${elm.name}</span></a>
<button class="popup-code-copy-button">copy</button>
<span class="popup-code">`
);
const decorate = (elm, state) => {
switch (elm.type) {
case INDENT:
// codeblockはcodeblockの開始タグ + 1 レベル深いインデント以上なら継続する
if (IN_CODEBLOCK && CURRENT_INDENT_LEVEL + 1 > elm.level) {
IN_CODEBLOCK = false;
DO_CODEBLOCK_ESCAPE_PROCESS = true; // 閉じタグを追加する
}
CURRENT_INDENT_LEVEL = elm.level;
// インデント1階層につき半角スペース2個分あける
return ' '.repeat(elm.level);
case LINK:
// TODO: リンク処理
if (elm.internal || getDomain(elm.url) === SCRAPBOX_DOMAIN) {
return makeInternalLinkTag(elm);
}
return makeLinkTag(elm);
case DECORATION:
// 文字装飾
return makeDecorationTag(elm);
case BACKQUOTE:
// 行コード
return makeBackQuoteTag(elm);
case TEXT:
// 自由入力テキスト
if (DO_CODEBLOCK_ESCAPE_PROCESS) {
DO_CODEBLOCK_ESCAPE_PROCESS = false;
return ${elm.text}</span></span>;
}
return elm.text;
case HASH:
// TODO: ハッシュリンク処理
return makeHashLinkTag(elm);
case QUOTE:
// TODO: 引用処理
return makeQuoteTag(elm);
case TEX:
// TODO: 数式処理
return elm.text;
case SHELL:
// シェル記法
return makeShellTag(elm);
case CODEBLOCK:
// コードブロック処理開始
IN_CODEBLOCK = true;
return makeCodeStartTag(elm, state);
case TABLE:
// TODO: テーブル表記処理
return elm.name;
case BLANK:
// 改行処理
return '<br />';
case CONTENTS:
// コンテンツ処理(再帰処理)
let html = '';
elm.contents.forEach(elm => {
html += decorate(elm, state);
});
return html;
default:
console.warn(unknown result type: ${elm.type});
}
return '';
};
// process は parser の結果から html を生成して返す
const process = (result, state) => {
let html = '';
result.lines.forEach(line => {
// TODO: パーサーのできが悪くnullが含まれる場合があるため確認する
if (!line) { return; }
// TODO: パーサーの返り値が統一されていないためオブジェクトか逐次確認する
if (line.type && !Array.isArray(line)) {
html += decorate(line, state);
} else {
line.forEach(elm => {
html += decorate(elm, state);
});
}
});
return html;
};
/* ================ */
/* 表示コントール */
/* ================ */
const previewPageText = (page, popup, title, lineHash, emptyLink, category) => {
let extraClassName = '';
const isExternalProject = PROJECT_NAME !== getProjectName();
let url = https://scrapbox.io/api/pages/${PROJECT_NAME}/${title};
if (emptyLink) {
if (category) {
title = ${category}%2F${title};
}
url = https://scrapbox.io/api/pages/${REFERENCE_PROJECT}/${title};
}
$.ajax({
type: 'GET',
contentType: 'application/json',
url,
}).done(data => {
// EMPTY_LINKS = data.emptyLinks || [];
if (isExternalProject) {
popup.addClass('popup-external-project');
}
popup.attr('data-project', PROJECT_NAME);
// ここでDOM要素にポップアップを追加する
page.append(popup);
const lines = data.lines;
const contents = [];
const state = {title: data.title, projectName: PROJECT_NAME};
let result;
let html = '';
for (let l = 1; l < lines.length; l++) {
if (!line.text) {
contents.push('');
continue;
}
try {
result = parser.parse(line.text);
} catch (e) {
// ParseError
console.warn(e);
continue;
}
if (result && result.lines) {
//console.log(result.lines); // TODO: あとで消す
html = process(result, state);
contents.push(html);
}
if (lineHash && line.id === lineHash) {
extraClassName = 'popup-line-permalink';
break
}
}
// popup を表示する
if (contents.length > 0) {
popup.html(<div class="${extraClassName}"><h5 class="popup-title">${data.title}</h5>${contents.join('<br />')}</div>);
popup.show();
}
});
};
const eliminateHashes = (title, projectName) => {
title = title.replace(/^#/, ''); // 先頭のハッシュタグ記号があれば取り除く
// エディタ左側をクリックしたときにURLに付与されるラインハッシュ(テロメア)の処理
let match = title.match(/#.{24,32}$/); // 先頭#で始まる24~34文字の文字列が含まれているか?
let lineHash = null;
if (match !== null) {
title = title.replace(/#.{24,32}$/, ''); // 含まれていたらtitleから消す
lineHash = match0.replace('#', ''); // ハッシュの先頭の#を削除 }
if (title.startsWith('/')) {
// 外部プロジェクト名とページ名を抽出
let tt = title.match(/\/(^\/+)\/(.+)/); if (!tt) return {title: '', lineHash: ''};
// console.log('title startsWith "/"', title, lineHash, projectName);
}
title = encodeURIComponent(title);
// TODO: グローバル変数依存をなくす
PROJECT_NAME = projectName; // プロジェクト名(グローバル変数)の書き換え
return {
title,
lineHash,
};
};
// 拡張機能の起点となるイベントリスナの登録はここ
export const enablePopup = () => {
const root = $('#app-container'); // <body>の直下にあるのは $appRoot
let timer = null;
root.on('mouseenter', 'a.page-link', (e) => {
let aTag = $(e.target).closest('a.page-link'); // 起点となっている <a>
let parent = $(e.target).closest('div.text-popup'); // 親ポップアップ
let page = root.find('.page'); // page は Scrapbox のエディタ部分(白背景)
let emptyLink = false;
// 空リンクは ref を参照する
let category = '';
if (aTag.hasClass('empty-page-link')) {
emptyLink = true;
//console.log(tags);
if (tags && tags.length) {
category = tags0.innerText.substr(1); }
//console.log(category);
}
// ポップアップの雛形エレメントの用意
let popup = $(<div class="popup-text-bubble related-page-list popup-card popup-card-root"></div>);
// マウスオーバーしている <a> のCSSボーダーボックスを取得する(x, y には現在座標が入っている)
let rect = aTag0.getBoundingClientRect(); // ポップアップのデザイン
popup.css({
'max-width': $('.editor')0.offsetWidth - aTag0.offsetLeft, 'left': rect.left + window.pageXOffset,
'top': 18 + rect.top + window.pageYOffset + aTag0.offsetHeight + 3 - 24, 'border-color': $('body').css('background-color')
});
// 表示位置の取得
let pos = ${popup.css('top')}_${popup.css('left')};
popup.attr('data-pos', pos);
// すでに表示されているならば, 何もしない
if ($(.text-popup[data-pos="${pos}"]).length > 0) {
return;
}
if (aTag.attr('rel') && aTag.attr('rel') === 'route') {
$(.text-popup:not([data-pos="${pos}"])).remove();
}
// キーワード先のテキストを取得する
let keyword = aTag0.innerText; // タグ名を取得 timer = setTimeout(() => {
// 親ポップアップがあったらScrapboxプロジェクト名は親から取得する
let projectName = parent.length > 0 ? parent.attr('data-project') : getProjectName();
const title = eliminateHashes(keyword.trim(), projectName);
console.log(title);
previewPageText(page, popup, title.title, title.lineHash, emptyLink, category);
}, 650);
});
root.on('click', 'button.popup-code-copy-button', e => {
const text = $($(e.target)0.parentElement).find('span.popup-code')0.innerText; copyToClipboard(text);
});
// イベントリスナーの登録
root.on('mouseleave', 'a.page-link', (e) => {
clearTimeout(timer);
});
root.on('mouseleave', '.popup-card', (e) => {
clearTimeout(timer);
});
root.on('click', (e) => {
clearTimeout(timer);
let popup = $('.popup-card');
let t = $(e.target).closest('.popup-card');
if ($(e.target)0.tagName.toLowerCase() === 'a') { popup.remove();
} else if (t.length > 0) {
//t.remove();
} else {
popup.remove();
}
})
};
enablePopup();
code:script.js
$(function(){
console.log('Hello');
});
table:test
a b c
あ い う
code:style.css
/* セル間に線を入れる from:テーブルの見た目をカスタマイズ */
.table-block .cell {
/* 全てのセルの右と下 */
}
.table-block .cell:first-child {
/* 1列目のセルの左 */
}
.section-title + .line .table-block .cell {
/* 1行目のセルの上 */
}
/* 1行目を太字、中央揃え */
.section-title + .line .table-block .cell {
font-weight: bolder;
text-align: center;
}