-
Notifications
You must be signed in to change notification settings - Fork 21
/
sync-inclusion-bot-words.js
124 lines (105 loc) · 4.73 KB
/
sync-inclusion-bot-words.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
const fs = require("fs/promises");
const jsYaml = require("js-yaml");
// The frontmatter here is all of the comments at the beginning of the file.
// Once there's a non-comment line, the frontmatter ends.
const getYamlFrontmatter = (yaml) => {
const firstNonComment = yaml
.split("\n")
.findIndex((line) => !line.startsWith("#"));
const frontmatter = yaml.split("\n").slice(0, firstNonComment).join("\n");
return frontmatter;
};
// The parsing works by finding lines that start with a vertical pipe, a
// space, and anything other than a dash. The vertical pipe represents the
// start of a markdown table row, which we want, and the dash indicates
// that what we're seeing is actually the divider between the table header
// and the table body.
const getMarkdownTableRows = (md) =>
md.match(/\| [^-].+\|/gi).map((v) => v.split("|"));
// Rows are split along the vertical pipes, which gives us an empty string
// before the pipe, so given the whole row, we'll just ignore the first item.
const markdownRowToTrigger = ([, matches, alternatives, why]) => ({
// Trigger matches are separated by commas, so we'll split them along
// commas and trim any remaining whitespace
matches: matches.split(",").map((v) => v.trim().toLowerCase()),
// Suggested alternatives are separated by line breaks, because they can
// include punctuation, including commas. Similarly split and trim.
alternatives: alternatives.split("<br>").map((v) => v.trim()),
// The "why" text can contain some extra reading, after a pair of line
// breaks, but the bot shouldn't show that because it's too much content
// to display. So we strip that out. The "why" also uses "this term" in
// most places to refer to the triggering phrase, but the bot is capable
// of showing the user exactly what word triggered its response. So we
// replace "this term" with the ":TERM:" token (in quotes, so it will be
// quoted when the user sees it) to let the bot do that.
why: why
.trim()
.replace(/<br>.*/, "")
.replace(/this term/i, '":TERM:"'),
});
const getTriggerIgnoreMap = (currentConfig) => {
const triggerWithIgnore = (newTrigger) => {
// Find a trigger in the existing config that has at least one of the same
// matches as the new trigger. There may not be one, but if...
const existing = currentConfig.triggers.find(
({ matches: currentMatches }) =>
currentMatches.some((v) => newTrigger.matches.includes(v)),
);
// If there is an existing config that matches AND it has an ignore property
// return a new trigger that includes the ignore property. (We do it this
// way instead of using an object spread so we get the properties in the
// order we want for readability when it gets written to yaml.)
if (existing?.ignore) {
return {
matches: newTrigger.matches,
alternatives: newTrigger.alternatives,
ignore: existing.ignore,
why: newTrigger.why,
};
}
// Otherwise just return what we got.
return newTrigger;
};
return triggerWithIgnore;
};
const main = async () => {
// Load the current config yaml and prase it into Javascript
const currentYamlStr = await fs.readFile("src/scripts/inclusion-bot.yaml", {
encoding: "utf-8",
});
const currentConfig = jsYaml.load(currentYamlStr, { json: true });
// Also find the frontmatter comments so we can preserve it.
const frontmatter = getYamlFrontmatter(currentYamlStr);
const triggerWithIgnore = getTriggerIgnoreMap(currentConfig);
// Read and parse the markdown.
const mdf = await fs.readFile("InclusionBot.md", { encoding: "utf-8" });
const md = {
// Preserve the link and message properties from the current config...
link: currentConfig.link,
message: currentConfig.message,
// ...but rebuild the triggers from the markdown.
triggers: getMarkdownTableRows(mdf)
// Turn each row into a trigger object.
.map(markdownRowToTrigger)
// Add an ignore property if there's a corresponding trigger in the
// existing config that has an ignore property. This is how we make sure
// we don't lose those when the new config is built.
.map(triggerWithIgnore)
// And remove the "triggers" that include "instead of", which is actually
// the table header.
.filter(({ matches }) => !/^instead of/i.test(matches.join(","))),
};
// Parse the object back into a yaml string
const configYaml = jsYaml
.dump(md)
// Put in some newlines between things, so it looks nicer.
.replace(/\n([a-z]+):/gi, "\n\n$1:")
.replace(/ {2}- matches/gi, "\n - matches");
// And write that puppy to disk
await fs.writeFile(
"src/scripts/inclusion-bot.yaml",
[frontmatter, "", configYaml].join("\n"),
{ encoding: "utf-8" },
);
};
main();