@korap/cmc-tagger 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/Readme.md +187 -0
- package/package.json +57 -0
- package/src/emoji_data.json +36577 -0
- package/src/index.js +206 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const packageVersion = require('../package.json').version;
|
|
4
|
+
|
|
5
|
+
const emoticonRegex = /^(\:\w+\:|\<[\/\\]?3|[\(\)\\\D|\*\$][\-\^]?[\:\;\=]|[\:\;\=B8][\-\^]?[3DOPp\@\$\*\\\)\(\/\|])(?=\s|[\!\.\?]|$)/;
|
|
6
|
+
const hashtagRegex = /^#(?=.*\p{L})[\p{L}\p{M}\p{N}]+$/u;
|
|
7
|
+
const urlRegex = /^(ftp|http)s?:\/\/[^\s]+/;
|
|
8
|
+
const emailRegex = /^\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/;
|
|
9
|
+
const addressRegex = /^@[a-zA-Z0-9]+/;
|
|
10
|
+
const actionWordRegex = /^:[^:]+:$/;
|
|
11
|
+
const wikiEmojiRegex = /^\[_EMOJI:[^\]]+\]$/;
|
|
12
|
+
|
|
13
|
+
// Load emoji data
|
|
14
|
+
let emojiData = {};
|
|
15
|
+
try {
|
|
16
|
+
emojiData = require('./emoji_data.json');
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// Silent fallback if file doesn't exist (e.g. during initial setup before script run)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Function to strip emoji modifiers and zero-width joiners to get base emoji
|
|
22
|
+
function getBaseEmoji(emoji) {
|
|
23
|
+
const stripped = emoji
|
|
24
|
+
// Remove skin tone modifiers (U+1F3FB-U+1F3FF)
|
|
25
|
+
.replace(/[\u{1F3FB}-\u{1F3FF}]/gu, '')
|
|
26
|
+
// Remove zero-width joiners (U+200D)
|
|
27
|
+
.replace(/\u200D/g, '')
|
|
28
|
+
// Remove variation selectors (U+FE0F, U+FE0E)
|
|
29
|
+
.replace(/[\uFE0E\uFE0F]/g, '');
|
|
30
|
+
|
|
31
|
+
// Extract the first emoji character using Array spread to handle multi-byte emoji
|
|
32
|
+
const chars = [...stripped];
|
|
33
|
+
return chars.length > 0 ? chars[0] : stripped;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const optionDefinitions = [
|
|
37
|
+
{ name: 'sparse', alias: 's', type: Boolean, description: 'Print only the files, lines that have POS annotations.' },
|
|
38
|
+
{ name: 'version', alias: 'V', type: Boolean, description: 'Print the current package version.' },
|
|
39
|
+
{ name: 'help', alias: 'h', type: Boolean, description: 'Print this usage guide.' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const sections = [
|
|
43
|
+
{
|
|
44
|
+
header: 'conllu-cmc',
|
|
45
|
+
content: 'Reads CoNLL-U format from stdin and annotates emojis, emoticons, hashtags, URLs, email addresses, @addresses, and action words. Writes CoNLL-U format to stdout.'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
header: 'Synopsis',
|
|
49
|
+
content: '$ conllu-cmc [-s] < input.conllu > output.conllu\n$ cmc-tagger [-s] < input.conllu > output.conllu\n$ conllu-cmc -V\n$ cmc-tagger -V'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
header: 'Options',
|
|
53
|
+
optionList: optionDefinitions
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const getUsage = require('command-line-usage')
|
|
58
|
+
const commandLineArgs = require('command-line-args')
|
|
59
|
+
|
|
60
|
+
var options;
|
|
61
|
+
try {
|
|
62
|
+
options = commandLineArgs(optionDefinitions)
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error(e.message);
|
|
65
|
+
options = { help: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (options.help) {
|
|
69
|
+
const usage = getUsage(sections);
|
|
70
|
+
console.log(usage);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (options.version) {
|
|
75
|
+
console.log(packageVersion);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const EmojiRegex = require('emoji-regex');
|
|
80
|
+
const emojiRegex = EmojiRegex();
|
|
81
|
+
const { once } = require('events');
|
|
82
|
+
const readline = require('readline');
|
|
83
|
+
global.header = '';
|
|
84
|
+
global.fileheader = '';
|
|
85
|
+
global.standalone = false
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
const rl = readline.createInterface({
|
|
89
|
+
input: process.stdin,
|
|
90
|
+
terminal: false,
|
|
91
|
+
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async function writeOutput(text) {
|
|
96
|
+
if (!process.stdout.write(text)) {
|
|
97
|
+
await once(process.stdout, 'drain');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async function parseConllu(line) {
|
|
103
|
+
if (line.match('#\\s*foundry')) {
|
|
104
|
+
if (line.match('=\\s*base')) {
|
|
105
|
+
if (options.sparse) {
|
|
106
|
+
global.standalone = true
|
|
107
|
+
}
|
|
108
|
+
await writeOutput("# foundry = cmc\n");
|
|
109
|
+
} else {
|
|
110
|
+
await writeOutput(`${line}\n`);
|
|
111
|
+
}
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (global.standalone) {
|
|
116
|
+
if (line.match('^#\\s*filename')) {
|
|
117
|
+
global.fileheader = `${line}\n`;
|
|
118
|
+
return;
|
|
119
|
+
} else if (line.match('^#\\s*text_id')) {
|
|
120
|
+
global.fileheader += `${line}\n`;
|
|
121
|
+
return;
|
|
122
|
+
} else if (line.match('^#\\s*eo[ft]')) {
|
|
123
|
+
await writeOutput(`${line}\n`);
|
|
124
|
+
return;
|
|
125
|
+
} else if (line.match('^#')) {
|
|
126
|
+
global.header += `${line}\n`;
|
|
127
|
+
return;
|
|
128
|
+
} else if (line.trim().match('^$')) {
|
|
129
|
+
if (global.header == "") {
|
|
130
|
+
await writeOutput("\n");
|
|
131
|
+
}
|
|
132
|
+
global.header = '';
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
if (!line.match('^\\d+')) {
|
|
137
|
+
await writeOutput(`${line}\n`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const columns = line.trim().split('\t');
|
|
143
|
+
|
|
144
|
+
const word = columns[1];
|
|
145
|
+
// Guard clause: if word is undefined, just output the line as-is
|
|
146
|
+
if (!word) {
|
|
147
|
+
await writeOutput(`${line}\n`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var new_tag = null;
|
|
152
|
+
if (word.match(wikiEmojiRegex)) {
|
|
153
|
+
new_tag = 'EMOWIKI';
|
|
154
|
+
} else if (word.match(emojiRegex)) {
|
|
155
|
+
new_tag = 'EMOIMG';
|
|
156
|
+
} else if (word.match(actionWordRegex)) {
|
|
157
|
+
new_tag = 'AKW';
|
|
158
|
+
} else if (word.match(emoticonRegex)) {
|
|
159
|
+
new_tag = 'EMOASC';
|
|
160
|
+
} else if (word.match(hashtagRegex)) {
|
|
161
|
+
new_tag = 'HST';
|
|
162
|
+
} else if (word.match(urlRegex)) {
|
|
163
|
+
new_tag = 'URL';
|
|
164
|
+
} else if (word.match(emailRegex)) {
|
|
165
|
+
new_tag = 'EML';
|
|
166
|
+
} else if (word.match(addressRegex)) {
|
|
167
|
+
new_tag = 'ADR';
|
|
168
|
+
}
|
|
169
|
+
if (new_tag) {
|
|
170
|
+
columns[4] = new_tag;
|
|
171
|
+
columns[5] = '_';
|
|
172
|
+
// For EMOIMG tokens, set lemma to the base emoji (without modifiers)
|
|
173
|
+
if (new_tag === 'EMOIMG') {
|
|
174
|
+
const base = getBaseEmoji(word);
|
|
175
|
+
columns[2] = base;
|
|
176
|
+
|
|
177
|
+
// Look up emoji metadata
|
|
178
|
+
// Try exact match first, then base emoji
|
|
179
|
+
const data = emojiData[word] || emojiData[base];
|
|
180
|
+
if (data) {
|
|
181
|
+
// g=group|s=subgroup|q=qualified|v=version|n=name
|
|
182
|
+
columns[5] = `g=${data.g}|s=${data.s}|q=${data.q}|v=${data.v}|n=${data.n}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (global.standalone) {
|
|
186
|
+
await writeOutput(fileheader);
|
|
187
|
+
await writeOutput(header);
|
|
188
|
+
new_tag = null;
|
|
189
|
+
header = fileheader = '';
|
|
190
|
+
}
|
|
191
|
+
await writeOutput(columns.join('\t') + '\n');
|
|
192
|
+
} else if (!global.standalone) {
|
|
193
|
+
await writeOutput(`${line}\n`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function main() {
|
|
198
|
+
for await (const line of rl) {
|
|
199
|
+
await parseConllu(line);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
main().catch((error) => {
|
|
204
|
+
console.error(error);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|