Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions browser/websocket/__snapshots__/findMetadata.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export const snapshot = {};

snapshot[`findMetadata() 1`] = `
[
[
"ふつうの",
"リンク2",
"hashtag",
],
[
"/help-jp/外部リンク",
],
[
"scrapbox",
"takker",
],
"https://scrapbox.io/files/65f29c24974fd8002333b160.svg",
[
"65f29c24974fd8002333b160",
"65e7f82e03949c0024a367d0",
"65e7f4413bc95600258481fb",
],
[
"助けてhelpfeel!!",
],
[
"名前 [scrapbox.icon]",
"住所 [リンク2]を入れること",
"電話番号 #をつけてもリンクにならないよ",
"自分の強み 3個くらい列挙",
],
]
`;
35 changes: 35 additions & 0 deletions browser/websocket/findMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { findMetadata, getHelpfeels } from "./findMetadata.ts";
import { assertEquals, assertSnapshot } from "../../deps/testing.ts";

const text = `てすと
[ふつうの]リンク
 しかし\`これは[リンク]\`ではない

code:code
コードブロック中の[リンク]や画像[https://scrapbox.io/files/65f29c0c9045b5002522c8bb.svg]は無視される


? 助けてhelpfeel!!

table:infobox
名前 [scrapbox.icon]
住所 [リンク2]を入れること
電話番号 #をつけてもリンクにならないよ
自分の強み 3個くらい列挙

#hashtag もつけるといいぞ?
[/forum-jp]のようなリンクは対象外
[/help-jp/]もだめ
[/icons/なるほど.icon][takker.icon]
[/help-jp/外部リンク]

サムネを用意
[https://scrapbox.io/files/65f29c24974fd8002333b160.svg]

[https://scrapbox.io/files/65e7f4413bc95600258481fb.svg https://scrapbox.io/files/65e7f82e03949c0024a367d0.svg]`;

Deno.test("findMetadata()", (t) => assertSnapshot(t, findMetadata(text)));
Deno.test("getHelpfeels()", () =>
assertEquals(getHelpfeels(text.split("\n").map((text) => ({ text }))), [
"助けてhelpfeel!!",
]));
179 changes: 179 additions & 0 deletions browser/websocket/findMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { BaseLine, Node, parse } from "../../deps/scrapbox.ts";
import { toTitleLc } from "../../title.ts";
import { parseYoutube } from "../../parser/youtube.ts";

/** テキストに含まれているメタデータを取り出す
*
* @param text Scrapboxのテキスト
* @return 順に、links, projectLinks, icons, image, files, helpfeels, infoboxDefinition
*/
export const findMetadata = (
text: string,
): [
string[],
string[],
string[],
string | null,
string[],
string[],
string[],
] => {
const blocks = parse(text, { hasTitle: true }).flatMap((block) => {
switch (block.type) {
case "codeBlock":
case "title":
return [];
case "line":
case "table":
return block;
}
});

/** 重複判定用map
*
* bracket link とhashtagを区別できるようにしている
* - bracket linkならtrue
*
* linkの形状はbracket linkを優先している
*/
const linksLc = new Map<string, boolean>();
const links = [] as string[];
const projectLinksLc = new Set<string>();
const projectLinks = [] as string[];
const iconsLc = new Set<string>();
const icons = [] as string[];
let image: string | null = null;
const files = new Set<string>();
const helpfeels = new Set<string>();

const fileUrlPattern = new RegExp(
`${
location?.origin ?? "https://scrapbox.io"
}/files/([a-z0-9]{24})(?:|\\.[a-zA-Z0-9]+)(?:|\\?[^\\s]*)$`,
);

const lookup = (node: Node) => {
switch (node.type) {
case "hashTag":
if (linksLc.has(toTitleLc(node.href))) return;
linksLc.set(toTitleLc(node.href), false);
links.push(node.href);
return;
case "link":
switch (node.pathType) {
case "relative": {
const link = cutId(node.href);
if (linksLc.get(toTitleLc(link))) return;
linksLc.set(toTitleLc(link), true);
links.push(link);
return;
}
case "root": {
const link = cutId(node.href);
// ignore `/project` or `/project/`
if (/^\/[\w\d-]+\/?$/.test(link)) return;
if (projectLinksLc.has(toTitleLc(link))) return;
projectLinksLc.add(toTitleLc(link));
projectLinks.push(link);
return;
}
case "absolute": {
const props = parseYoutube(node.href);
if (props && props.pathType !== "list") {
image ??= `https://i.ytimg.com/vi/${props.videoId}/mqdefault.jpg`;
return;
}
const fileId = node.href.match(fileUrlPattern)?.[1];
if (fileId) files.add(fileId);
return;
}
default:
return;
}
case "icon":
case "strongIcon": {
if (node.pathType === "root") return;
if (iconsLc.has(toTitleLc(node.path))) return;
iconsLc.add(toTitleLc(node.path));
icons.push(node.path);
return;
}
case "image":
case "strongImage": {
image ??= node.src.endsWith("/thumb/1000")
? node.src.replace(/\/thumb\/1000$/, "/raw")
: node.src;
{
const fileId = node.src.match(fileUrlPattern)?.[1];
if (fileId) files.add(fileId);
}
if (node.type === "image") {
const fileId = node.link.match(fileUrlPattern)?.[1];
if (fileId) files.add(fileId);
}
return;
}
case "helpfeel":
helpfeels.add(node.text);
return;
case "numberList":
case "strong":
case "quote":
case "decoration": {
for (const n of node.nodes) {
lookup(n);
}
return;
}
default:
return;
}
};

const infoboxDefinition = [] as string[];

for (const block of blocks) {
switch (block.type) {
case "line":
for (const node of block.nodes) {
lookup(node);
}
continue;
case "table": {
for (const row of block.cells) {
for (const nodes of row) {
for (const node of nodes) {
lookup(node);
}
}
}
if (!["infobox", "cosense"].includes(block.fileName)) continue;
infoboxDefinition.push(
...block.cells.map((row) =>
row.map((cell) => cell.map((node) => node.raw).join("")).join("\t")
.trim()
),
);
continue;
}
}
}

return [
links,
projectLinks,
icons,
image,
[...files],
[...helpfeels],
infoboxDefinition,
];
};

const cutId = (link: string): string => link.replace(/#[a-f\d]{24,32}$/, "");

/** テキストからHelpfeel記法のentryだけ取り出す */
export const getHelpfeels = (lines: Pick<BaseLine, "text">[]): string[] =>
lines.flatMap(({ text }) =>
/^\s*\? .*$/.test(text) ? [text.trimStart().slice(2)] : []
);
10 changes: 10 additions & 0 deletions browser/websocket/isSameArray.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { isSameArray } from "./isSameArray.ts";
import { assert } from "../../deps/testing.ts";

Deno.test("isSameArray()", () => {
assert(isSameArray([1, 2, 3], [1, 2, 3]));
assert(isSameArray([1, 2, 3], [3, 2, 1]));
assert(!isSameArray([1, 2, 3], [3, 2, 3]));
assert(!isSameArray([1, 2, 3], [1, 2]));
assert(isSameArray([], []));
});
2 changes: 2 additions & 0 deletions browser/websocket/isSameArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const isSameArray = <T>(a: T[], b: T[]): boolean =>
a.length === b.length && a.every((x) => b.includes(x));
Loading