@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/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
+ });