123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251 | // Logic for text pattern matching
#include <ctype.h>
#include <gc.h>
#include <string.h>
#include <strings.h>
#include <sys/param.h>
#include <unictype.h>
#include <uniname.h>
#include <unistring/version.h>
#define MAX_BACKREFS 100
#ifndef new
#define new(t, ...) ((t *)memcpy(GC_MALLOC(sizeof(t)), &(t){__VA_ARGS__}, sizeof(t)))
#endif
typedef struct {
Text_t text;
Int_t index;
List_t captures;
} PatternMatch;
typedef struct {
Text_t text;
Int_t index;
List_t captures;
bool is_none : 1;
} OptionalPatternMatch;
#define NONE_MATCH ((OptionalPatternMatch){.is_none = true})
typedef struct {
int64_t index, length;
bool occupied, recursive;
} capture_t;
typedef struct {
enum { PAT_START, PAT_END, PAT_ANY, PAT_GRAPHEME, PAT_PROPERTY, PAT_QUOTE, PAT_PAIR, PAT_FUNCTION } tag;
bool negated, non_capturing;
int64_t min, max;
union {
int32_t grapheme;
uc_property_t property;
int64_t (*fn)(TextIter_t *, int64_t);
int32_t quote_graphemes[2];
int32_t pair_graphemes[2];
};
} pat_t;
static Text_t replace_list(Text_t text, List_t replacements, Text_t backref_marker, bool recursive);
static INLINE void skip_whitespace(TextIter_t *state, int64_t *i) {
while (*i < state->stack[0].text.length) {
int32_t grapheme = Text$get_grapheme_fast(state, *i);
if (grapheme > 0 && !uc_is_property_white_space((ucs4_t)grapheme)) return;
*i += 1;
}
}
static INLINE bool match_grapheme(TextIter_t *state, int64_t *i, int32_t grapheme) {
if (*i < state->stack[0].text.length && Text$get_grapheme_fast(state, *i) == grapheme) {
*i += 1;
return true;
}
return false;
}
static INLINE bool match_str(TextIter_t *state, int64_t *i, const char *str) {
int64_t matched = 0;
while (matched[str]) {
if (*i + matched >= state->stack[0].text.length || Text$get_grapheme_fast(state, *i + matched) != str[matched])
return false;
matched += 1;
}
*i += matched;
return true;
}
static int64_t parse_int(TextIter_t *state, int64_t *i) {
int64_t value = 0;
for (;; *i += 1) {
uint32_t grapheme = Text$get_main_grapheme_fast(state, *i);
int digit = uc_digit_value(grapheme);
if (digit < 0) break;
if (value >= INT64_MAX / 10) break;
value = 10 * value + digit;
}
return value;
}
static const char *get_property_name(TextIter_t *state, int64_t *i) {
skip_whitespace(state, i);
char *name = GC_MALLOC_ATOMIC(UNINAME_MAX);
char *dest = name;
while (*i < state->stack[0].text.length) {
int32_t grapheme = Text$get_grapheme_fast(state, *i);
if (!(grapheme & ~0xFF) && (isalnum(grapheme) || grapheme == ' ' || grapheme == '_' || grapheme == '-')) {
*dest = (char)grapheme;
++dest;
if (dest >= name + UNINAME_MAX - 1) break;
} else {
break;
}
*i += 1;
}
while (dest > name && dest[-1] == ' ')
*(dest--) = '\0';
if (dest == name) return NULL;
*dest = '\0';
return name;
}
#define EAT1(state, index, cond) \
({ \
int32_t grapheme = Text$get_grapheme_fast(state, index); \
bool success = (cond); \
if (success) index += 1; \
success; \
})
#define EAT2(state, index, cond1, cond2) \
({ \
int32_t grapheme = Text$get_grapheme_fast(state, index); \
bool success = (cond1); \
if (success) { \
grapheme = Text$get_grapheme_fast(state, index + 1); \
success = (cond2); \
if (success) index += 2; \
} \
success; \
})
#define EAT_MANY(state, index, cond) \
({ \
int64_t _n = 0; \
while (EAT1(state, index, cond)) { \
_n += 1; \
} \
_n; \
})
static int64_t match_email(TextIter_t *state, int64_t index) {
// email = local "@" domain
// local = 1-64 ([a-zA-Z0-9!#$%&‘*+–/=?^_`.{|}~] | non-ascii)
// domain = dns-label ("." dns-label)*
// dns-label = 1-63 ([a-zA-Z0-9-] | non-ascii)
if (index > 0) {
uint32_t prev_codepoint = Text$get_main_grapheme_fast(state, index - 1);
if (uc_is_property_alphabetic(prev_codepoint)) return -1;
}
int64_t start_index = index;
// Local part:
int64_t local_len = 0;
static const char *allowed_local = "!#$%&‘*+–/=?^_`.{|}~";
while (EAT1(state, index, (grapheme & ~0x7F) || isalnum((char)grapheme) || strchr(allowed_local, (char)grapheme))) {
local_len += 1;
if (local_len > 64) return -1;
}
if (!EAT1(state, index, grapheme == '@')) return -1;
// Host
int64_t host_len = 0;
do {
int64_t label_len = 0;
while (EAT1(state, index, (grapheme & ~0x7F) || isalnum((char)grapheme) || grapheme == '-')) {
label_len += 1;
if (label_len > 63) return -1;
}
if (label_len == 0) return -1;
host_len += label_len;
if (host_len > 255) return -1;
host_len += 1;
} while (EAT1(state, index, grapheme == '.'));
return index - start_index;
}
static int64_t match_ipv6(TextIter_t *state, int64_t index) {
if (index > 0) {
int32_t prev_codepoint = Text$get_grapheme_fast(state, index - 1);
if ((prev_codepoint & ~0x7F) && (isxdigit(prev_codepoint) || prev_codepoint == ':')) return -1;
}
int64_t start_index = index;
const int NUM_CLUSTERS = 8;
bool double_colon_used = false;
for (int cluster = 0; cluster < NUM_CLUSTERS; cluster++) {
for (int digits = 0; digits < 4; digits++) {
if (!EAT1(state, index, ~(grapheme & ~0x7F) && isxdigit((char)grapheme))) break;
}
if (EAT1(state, index, ~(grapheme & ~0x7F) && isxdigit((char)grapheme))) return -1; // Too many digits
if (cluster == NUM_CLUSTERS - 1) {
break;
} else if (!EAT1(state, index, grapheme == ':')) {
if (double_colon_used) break;
return -1;
}
if (EAT1(state, index, grapheme == ':')) {
if (double_colon_used) return -1;
double_colon_used = true;
}
}
return index - start_index;
}
static int64_t match_ipv4(TextIter_t *state, int64_t index) {
if (index > 0) {
int32_t prev_codepoint = Text$get_grapheme_fast(state, index - 1);
if ((prev_codepoint & ~0x7F) && (isdigit(prev_codepoint) || prev_codepoint == '.')) return -1;
}
int64_t start_index = index;
const int NUM_CLUSTERS = 4;
for (int cluster = 0; cluster < NUM_CLUSTERS; cluster++) {
for (int digits = 0; digits < 3; digits++) {
if (!EAT1(state, index, ~(grapheme & ~0x7F) && isdigit((char)grapheme))) {
if (digits == 0) return -1;
break;
}
}
if (EAT1(state, index, ~(grapheme & ~0x7F) && isdigit((char)grapheme))) return -1; // Too many digits
if (cluster == NUM_CLUSTERS - 1) break;
else if (!EAT1(state, index, grapheme == '.')) return -1;
}
return (index - start_index);
}
static int64_t match_ip(TextIter_t *state, int64_t index) {
int64_t len = match_ipv6(state, index);
if (len >= 0) return len;
len = match_ipv4(state, index);
return (len >= 0) ? len : -1;
}
static int64_t match_host(TextIter_t *state, int64_t index) {
int64_t ip_len = match_ip(state, index);
if (ip_len > 0) return ip_len;
int64_t start_index = index;
if (match_grapheme(state, &index, '[')) {
ip_len = match_ip(state, index);
if (ip_len <= 0) return -1;
index += ip_len;
if (match_grapheme(state, &index, ']')) return (index - start_index);
return -1;
}
if (!EAT1(state, index, isalpha(grapheme))) return -1;
static const char *non_host_chars = "/#?:@ \t\r\n<>[]{}\\^|\"`";
EAT_MANY(state, index, (grapheme & ~0x7F) || !strchr(non_host_chars, (char)grapheme));
return (index - start_index);
}
static int64_t match_authority(TextIter_t *state, int64_t index) {
int64_t authority_start = index;
static const char *non_segment_chars = "/#?:@ \t\r\n<>[]{}\\^|\"`.";
// Optional user@ prefix:
int64_t username_len = EAT_MANY(state, index, (grapheme & ~0x7F) || !strchr(non_segment_chars, (char)grapheme));
if (username_len < 1 || !EAT1(state, index, grapheme == '@')) index = authority_start; // No user@ part
// Host:
int64_t host_len = match_host(state, index);
if (host_len <= 0) return -1;
index += host_len;
// Port:
if (EAT1(state, index, grapheme == ':')) {
if (EAT_MANY(state, index, !(grapheme & ~0x7F) && isdigit(grapheme)) == 0) return -1;
}
return (index - authority_start);
}
static int64_t match_uri(TextIter_t *state, int64_t index) {
// URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment]
// scheme = [a-zA-Z] [a-zA-Z0-9+.-]
// authority = [userinfo "@"] host [":" port]
if (index > 0) {
// Don't match if we're not at a word edge:
uint32_t prev_codepoint = Text$get_main_grapheme_fast(state, index - 1);
if (uc_is_property_alphabetic(prev_codepoint)) return -1;
}
int64_t start_index = index;
// Scheme:
if (!EAT1(state, index, isalpha(grapheme))) return -1;
EAT_MANY(state, index,
!(grapheme & ~0x7F) && (isalnum(grapheme) || grapheme == '+' || grapheme == '.' || grapheme == '-'));
if (!match_grapheme(state, &index, ':')) return -1;
// Authority:
int64_t authority_len;
if (match_str(state, &index, "//")) {
authority_len = match_authority(state, index);
if (authority_len > 0) index += authority_len;
} else {
authority_len = 0;
}
// Path:
int64_t path_start = index;
if (EAT1(state, index, grapheme == '/') || authority_len <= 0) {
static const char *non_path = " \"#?<>[]{}\\^`|";
EAT_MANY(state, index, (grapheme & ~0x7F) || !strchr(non_path, (char)grapheme));
if (EAT1(state, index, grapheme == '?')) { // Query
static const char *non_query = " \"#<>[]{}\\^`|";
EAT_MANY(state, index, (grapheme & ~0x7F) || !strchr(non_query, (char)grapheme));
}
if (EAT1(state, index, grapheme == '#')) { // Fragment
static const char *non_fragment = " \"#<>[]{}\\^`|";
EAT_MANY(state, index, (grapheme & ~0x7F) || !strchr(non_fragment, (char)grapheme));
}
}
if (authority_len <= 0 && index == path_start) return -1;
return index - start_index;
}
static int64_t match_url(TextIter_t *state, int64_t index) {
int64_t lookahead = index;
if (!(match_str(state, &lookahead, "https:") || match_str(state, &lookahead, "http:")
|| match_str(state, &lookahead, "ftp:") || match_str(state, &lookahead, "wss:")
|| match_str(state, &lookahead, "ws:")))
return -1;
return match_uri(state, index);
}
static int64_t match_id(TextIter_t *state, int64_t index) {
if (!EAT1(state, index, uc_is_property((ucs4_t)grapheme, UC_PROPERTY_XID_START))) return -1;
return 1 + EAT_MANY(state, index, uc_is_property((ucs4_t)grapheme, UC_PROPERTY_XID_CONTINUE));
}
static int64_t match_int(TextIter_t *state, int64_t index) {
int64_t negative = EAT1(state, index, grapheme == '-') ? 1 : 0;
int64_t len = EAT_MANY(state, index, uc_is_property((ucs4_t)grapheme, UC_PROPERTY_DECIMAL_DIGIT));
return len > 0 ? negative + len : -1;
}
static int64_t match_alphanumeric(TextIter_t *state, int64_t index) {
return EAT1(state, index, uc_is_property_alphabetic((ucs4_t)grapheme) || uc_is_property_numeric((ucs4_t)grapheme))
? 1
: -1;
}
static int64_t match_num(TextIter_t *state, int64_t index) {
bool negative = EAT1(state, index, grapheme == '-') ? 1 : 0;
int64_t pre_decimal = EAT_MANY(state, index, uc_is_property((ucs4_t)grapheme, UC_PROPERTY_DECIMAL_DIGIT));
bool decimal = (EAT1(state, index, grapheme == '.') == 1);
int64_t post_decimal =
decimal ? EAT_MANY(state, index, uc_is_property((ucs4_t)grapheme, UC_PROPERTY_DECIMAL_DIGIT)) : 0;
if (pre_decimal == 0 && post_decimal == 0) return -1;
return negative + pre_decimal + decimal + post_decimal;
}
static int64_t match_newline(TextIter_t *state, int64_t index) {
if (index >= state->stack[0].text.length) return -1;
uint32_t grapheme = index >= state->stack[0].text.length ? 0 : Text$get_main_grapheme_fast(state, index);
if (grapheme == '\n') return 1;
if (grapheme == '\r' && Text$get_grapheme_fast(state, index + 1) == '\n') return 2;
return -1;
}
static int64_t match_pat(TextIter_t *state, int64_t index, pat_t pat) {
Text_t text = state->stack[0].text;
int32_t grapheme = index >= text.length ? 0 : Text$get_grapheme_fast(state, index);
switch (pat.tag) {
case PAT_START: {
if (index == 0) return pat.negated ? -1 : 0;
return pat.negated ? 0 : -1;
}
case PAT_END: {
if (index >= text.length) return pat.negated ? -1 : 0;
return pat.negated ? 0 : -1;
}
case PAT_ANY: {
assert(!pat.negated);
return (index < text.length) ? 1 : -1;
}
case PAT_GRAPHEME: {
if (index >= text.length) return -1;
else if (grapheme == pat.grapheme) return pat.negated ? -1 : 1;
return pat.negated ? 1 : -1;
}
case PAT_PROPERTY: {
if (index >= text.length) return -1;
else if (uc_is_property((ucs4_t)grapheme, pat.property)) return pat.negated ? -1 : 1;
return pat.negated ? 1 : -1;
}
case PAT_PAIR: {
// Nested punctuation: (?), [?], etc
if (index >= text.length) return -1;
int32_t open = pat.pair_graphemes[0];
if (grapheme != open) return pat.negated ? 1 : -1;
int32_t close = pat.pair_graphemes[1];
int64_t depth = 1;
int64_t match_len = 1;
for (; depth > 0; match_len++) {
if (index + match_len >= text.length) return pat.negated ? 1 : -1;
int32_t c = Text$get_grapheme_fast(state, index + match_len);
if (c == open) depth += 1;
else if (c == close) depth -= 1;
}
return pat.negated ? -1 : match_len;
}
case PAT_QUOTE: {
// Nested quotes: "?", '?', etc
if (index >= text.length) return -1;
int32_t open = pat.quote_graphemes[0];
if (grapheme != open) return pat.negated ? 1 : -1;
int32_t close = pat.quote_graphemes[1];
for (int64_t i = index + 1; i < text.length; i++) {
int32_t c = Text$get_grapheme_fast(state, i);
if (c == close) {
return pat.negated ? -1 : (i - index) + 1;
} else if (c == '\\' && index + 1 < text.length) {
i += 1; // Skip ahead an extra step
}
}
return pat.negated ? 1 : -1;
}
case PAT_FUNCTION: {
int64_t match_len = pat.fn(state, index);
if (match_len >= 0) return pat.negated ? -1 : match_len;
return pat.negated ? 1 : -1;
}
default: errx(1, "Invalid pattern");
}
errx(1, "Unreachable");
return 0;
}
static pat_t parse_next_pat(TextIter_t *state, int64_t *index) {
if (EAT2(state, *index, uc_is_property((ucs4_t)grapheme, UC_PROPERTY_QUOTATION_MARK), grapheme == '?')) {
// Quotations: "?", '?', etc
int32_t open = Text$get_grapheme_fast(state, *index - 2);
int32_t close = open;
uc_mirror_char((ucs4_t)open, (ucs4_t *)&close);
if (!match_grapheme(state, index, close))
fail_text(Texts("Pattern's closing quote is missing: ", state->stack[0].text));
return (pat_t){
.tag = PAT_QUOTE,
.min = 1,
.max = 1,
.quote_graphemes = {open, close},
};
} else if (EAT2(state, *index, uc_is_property((ucs4_t)grapheme, UC_PROPERTY_PAIRED_PUNCTUATION), grapheme == '?')) {
// Nested punctuation: (?), [?], etc
int32_t open = Text$get_grapheme_fast(state, *index - 2);
int32_t close = open;
uc_mirror_char((ucs4_t)open, (ucs4_t *)&close);
if (!match_grapheme(state, index, close))
fail_text(Texts("Pattern's closing brace is missing: ", state->stack[0].text));
return (pat_t){
.tag = PAT_PAIR,
.min = 1,
.max = 1,
.pair_graphemes = {open, close},
};
} else if (EAT1(state, *index,
grapheme == '{')) { // named patterns {id}, {2-3 hex}, etc.
skip_whitespace(state, index);
int64_t min, max;
if (uc_is_digit((ucs4_t)Text$get_grapheme_fast(state, *index))) {
min = parse_int(state, index);
skip_whitespace(state, index);
if (match_grapheme(state, index, '+')) {
max = INT64_MAX;
} else if (match_grapheme(state, index, '-')) {
max = parse_int(state, index);
} else {
max = min;
}
if (min > max) fail_text(Texts("Minimum repetitions (", min, ") is less than the maximum (", max, ")"));
} else {
min = -1, max = -1;
}
skip_whitespace(state, index);
bool negated = match_grapheme(state, index, '!');
#define PAT(_tag, ...) ((pat_t){.min = min, .max = max, .negated = negated, .tag = _tag, __VA_ARGS__})
const char *prop_name;
if (match_str(state, index, "..")) prop_name = "..";
else prop_name = get_property_name(state, index);
if (!prop_name) {
// Literal character, e.g. {1?}
skip_whitespace(state, index);
int32_t grapheme = Text$get_grapheme_fast(state, (*index)++);
if (!match_grapheme(state, index, '}'))
fail_text(Texts("Missing closing '}' in pattern: ", state->stack[0].text));
return PAT(PAT_GRAPHEME, .grapheme = grapheme);
} else if (strlen(prop_name) == 1) {
// Single letter names: {1+ A}
skip_whitespace(state, index);
if (!match_grapheme(state, index, '}'))
fail_text(Texts("Missing closing '}' in pattern: ", state->stack[0].text));
return PAT(PAT_GRAPHEME, .grapheme = prop_name[0]);
}
skip_whitespace(state, index);
if (!match_grapheme(state, index, '}'))
fail_text(Texts("Missing closing '}' in pattern: ", state->stack[0].text));
switch (tolower(prop_name[0])) {
case '.':
if (prop_name[1] == '.') {
if (negated) return ((pat_t){.tag = PAT_END, .min = min, .max = max, .non_capturing = true});
else return PAT(PAT_ANY);
}
break;
case 'a':
if (strcasecmp(prop_name, "authority") == 0) {
return PAT(PAT_FUNCTION, .fn = match_authority);
} else if (strcasecmp(prop_name, "alphanum") == 0 || strcasecmp(prop_name, "anum") == 0
|| strcasecmp(prop_name, "alphanumeric") == 0) {
return PAT(PAT_FUNCTION, .fn = match_alphanumeric);
}
break;
case 'c':
if (strcasecmp(prop_name, "crlf") == 0) return PAT(PAT_FUNCTION, .fn = match_newline);
break;
case 'd':
if (strcasecmp(prop_name, "digit") == 0) {
return PAT(PAT_PROPERTY, .property = UC_PROPERTY_DECIMAL_DIGIT);
}
break;
case 'e':
if (strcasecmp(prop_name, "end") == 0) {
return PAT(PAT_END, .non_capturing = !negated);
} else if (strcasecmp(prop_name, "email") == 0) {
return PAT(PAT_FUNCTION, .fn = match_email);
}
#if _LIBUNISTRING_VERSION >= 0x0100000
else if (strcasecmp(prop_name, "emoji") == 0) {
return PAT(PAT_PROPERTY, .property = UC_PROPERTY_EMOJI);
}
#endif
break;
case 'h':
if (strcasecmp(prop_name, "host") == 0) {
return PAT(PAT_FUNCTION, .fn = match_host);
}
break;
case 'i':
if (strcasecmp(prop_name, "id") == 0) {
return PAT(PAT_FUNCTION, .fn = match_id);
} else if (strcasecmp(prop_name, "int") == 0) {
return PAT(PAT_FUNCTION, .fn = match_int);
} else if (strcasecmp(prop_name, "ipv4") == 0) {
return PAT(PAT_FUNCTION, .fn = match_ipv4);
} else if (strcasecmp(prop_name, "ipv6") == 0) {
return PAT(PAT_FUNCTION, .fn = match_ipv6);
} else if (strcasecmp(prop_name, "ip") == 0) {
return PAT(PAT_FUNCTION, .fn = match_ip);
}
break;
case 'l':
if (strcasecmp(prop_name, "letter") == 0) {
return PAT(PAT_PROPERTY, .property = UC_PROPERTY_ALPHABETIC);
}
case 'n':
if (strcasecmp(prop_name, "nl") == 0 || strcasecmp(prop_name, "newline") == 0) {
return PAT(PAT_FUNCTION, .fn = match_newline);
} else if (strcasecmp(prop_name, "num") == 0) {
return PAT(PAT_FUNCTION, .fn = match_num);
}
break;
case 's':
if (strcasecmp(prop_name, "start") == 0) {
return PAT(PAT_START, .non_capturing = !negated);
}
break;
case 'u':
if (strcasecmp(prop_name, "uri") == 0) {
return PAT(PAT_FUNCTION, .fn = match_uri);
} else if (strcasecmp(prop_name, "url") == 0) {
return PAT(PAT_FUNCTION, .fn = match_url);
}
break;
case 'w':
if (strcasecmp(prop_name, "word") == 0) {
return PAT(PAT_FUNCTION, .fn = match_id);
} else if (strcasecmp(prop_name, "ws") == 0 || strcasecmp(prop_name, "whitespace") == 0) {
return PAT(PAT_PROPERTY, .property = UC_PROPERTY_WHITE_SPACE);
}
break;
default: break;
}
uc_property_t prop = uc_property_byname(prop_name);
if (uc_property_is_valid(prop)) return PAT(PAT_PROPERTY, .property = prop);
ucs4_t grapheme = unicode_name_character(prop_name);
if (grapheme == UNINAME_INVALID) fail_text(Texts("Not a valid property or character name: ", prop_name));
return PAT(PAT_GRAPHEME, .grapheme = (int32_t)grapheme);
#undef PAT
} else {
return (pat_t){.tag = PAT_GRAPHEME,
.non_capturing = true,
.min = 1,
.max = 1,
.grapheme = Text$get_grapheme_fast(state, (*index)++)};
}
}
static int64_t match(Text_t text, int64_t text_index, Text_t pattern, int64_t pattern_index, capture_t *captures,
int64_t capture_index) {
if (pattern_index >= pattern.length) // End of the pattern
return 0;
int64_t start_index = text_index;
TextIter_t pattern_state = NEW_TEXT_ITER_STATE(pattern), text_state = NEW_TEXT_ITER_STATE(text);
pat_t pat = parse_next_pat(&pattern_state, &pattern_index);
if (pat.min == -1 && pat.max == -1) {
if (pat.tag == PAT_ANY && pattern_index >= pattern.length) {
pat.min = pat.max = MAX(1, text.length - text_index);
} else {
pat.min = 1;
pat.max = INT64_MAX;
}
}
int64_t capture_start = text_index;
int64_t count = 0, capture_len = 0, next_match_len = 0;
if (pat.tag == PAT_ANY && pattern_index >= pattern.length) {
int64_t remaining = text.length - text_index;
capture_len = remaining >= pat.min ? MIN(remaining, pat.max) : -1;
text_index += capture_len;
goto success;
}
if (pat.min == 0 && pattern_index < pattern.length) {
next_match_len =
match(text, text_index, pattern, pattern_index, captures, capture_index + (pat.non_capturing ? 0 : 1));
if (next_match_len >= 0) {
capture_len = 0;
goto success;
}
}
while (count < pat.max) {
int64_t match_len = match_pat(&text_state, text_index, pat);
if (match_len < 0) break;
capture_len += match_len;
text_index += match_len;
count += 1;
if (pattern_index < pattern.length) { // More stuff after this
if (count < pat.min) next_match_len = -1;
else
next_match_len = match(text, text_index, pattern, pattern_index, captures,
capture_index + (pat.non_capturing ? 0 : 1));
} else {
next_match_len = 0;
}
if (match_len == 0) {
if (next_match_len >= 0) {
// If we're good to go, no need to keep re-matching zero-length
// matches till we hit max:
count = pat.max;
break;
} else {
return -1;
}
}
if (pattern_index < pattern.length && next_match_len >= 0) break; // Next guy exists and wants to stop here
if (text_index >= text.length) break;
}
if (count < pat.min || next_match_len < 0) return -1;
success:
if (captures && capture_index < MAX_BACKREFS && !pat.non_capturing) {
if (pat.tag == PAT_PAIR || pat.tag == PAT_QUOTE) {
assert(capture_len > 0);
captures[capture_index] = (capture_t){
.index = capture_start + 1, // Skip leading quote/paren
.length = capture_len - 2, // Skip open/close
.occupied = true,
.recursive = (pat.tag == PAT_PAIR),
};
} else {
captures[capture_index] = (capture_t){
.index = capture_start,
.length = capture_len,
.occupied = true,
.recursive = false,
};
}
}
return (text_index - start_index) + next_match_len;
}
#undef EAT1
#undef EAT2
#undef EAT_MANY
static int64_t _find(Text_t text, Text_t pattern, int64_t first, int64_t last, int64_t *match_length,
capture_t *captures) {
int32_t first_grapheme = Text$get_grapheme(pattern, 0);
bool find_first = (first_grapheme != '{' && !uc_is_property((ucs4_t)first_grapheme, UC_PROPERTY_QUOTATION_MARK)
&& !uc_is_property((ucs4_t)first_grapheme, UC_PROPERTY_PAIRED_PUNCTUATION));
TextIter_t text_state = NEW_TEXT_ITER_STATE(text);
for (int64_t i = first; i <= last; i++) {
// Optimization: quickly skip ahead to first char in pattern:
if (find_first) {
while (i < text.length && Text$get_grapheme_fast(&text_state, i) != first_grapheme)
++i;
}
int64_t m = match(text, i, pattern, 0, captures, 0);
if (m >= 0) {
if (match_length) *match_length = m;
return i;
}
}
if (match_length) *match_length = -1;
return -1;
}
static OptionalPatternMatch find(Text_t text, Text_t pattern, Int_t from_index) {
int64_t first = Int64$from_int(from_index, false);
if (first == 0) fail_text(Text("Invalid index: 0"));
if (first < 0) first = text.length + first + 1;
if (first > text.length || first < 1) return NONE_MATCH;
capture_t captures[MAX_BACKREFS] = {};
int64_t len = 0;
int64_t found = _find(text, pattern, first - 1, text.length - 1, &len, captures);
if (found == -1) return NONE_MATCH;
List_t capture_list = {};
for (int i = 0; captures[i].occupied; i++) {
Text_t capture = Text$slice(text, I(captures[i].index + 1), I(captures[i].index + captures[i].length));
List$insert(&capture_list, &capture, I(0), sizeof(Text_t));
}
return (OptionalPatternMatch){
.text = Text$slice(text, I(found + 1), I(found + len)),
.index = I(found + 1),
.captures = capture_list,
};
}
PUREFUNC static bool Pattern$has(Text_t text, Text_t pattern) {
if (pattern.length == 0) {
return true;
} else if (Text$starts_with(pattern, Text("{start}"), &pattern)) {
int64_t m = match(text, 0, pattern, 0, NULL, 0);
return m >= 0;
} else if (Text$ends_with(text, Text("{end}"), NULL)) {
for (int64_t i = text.length - 1; i >= 0; i--) {
int64_t match_len = match(text, i, pattern, 0, NULL, 0);
if (match_len >= 0 && i + match_len == text.length) return true;
}
return false;
} else {
int64_t found = _find(text, pattern, 0, text.length - 1, NULL, NULL);
return (found >= 0);
}
}
static bool Pattern$matches(Text_t text, Text_t pattern) {
if (pattern.length == 0) return true;
capture_t captures[MAX_BACKREFS] = {};
int64_t match_len = match(text, 0, pattern, 0, NULL, 0);
return (match_len == text.length);
}
static bool Pattern$match_at(Text_t text, Text_t pattern, Int_t pos, PatternMatch *dest) {
if (pattern.length == 0) return true;
int64_t start = Int64$from_int(pos, false) - 1;
capture_t captures[MAX_BACKREFS] = {};
int64_t match_len = match(text, start, pattern, 0, captures, 0);
if (match_len < 0) return false;
List_t capture_list = {};
for (int i = 0; captures[i].occupied; i++) {
Text_t capture = Text$slice(text, I(captures[i].index + 1), I(captures[i].index + captures[i].length));
List$insert(&capture_list, &capture, I(0), sizeof(Text_t));
}
dest->text = Text$slice(text, I(start + 1), I(start + match_len));
dest->index = I(start + 1);
dest->captures = capture_list;
return true;
}
static OptionalList_t Pattern$captures(Text_t text, Text_t pattern) {
if (pattern.length == 0) return EMPTY_LIST;
capture_t captures[MAX_BACKREFS] = {};
int64_t match_len = match(text, 0, pattern, 0, captures, 0);
if (match_len != text.length) return NONE_LIST;
List_t capture_list = {};
for (int i = 0; captures[i].occupied; i++) {
Text_t capture = Text$slice(text, I(captures[i].index + 1), I(captures[i].index + captures[i].length));
List$insert(&capture_list, &capture, I(0), sizeof(Text_t));
}
return capture_list;
}
static List_t Pattern$find_all(Text_t text, Text_t pattern) {
if (text.length == 0 || pattern.length == 0) // special case
return EMPTY_LIST;
List_t matches = {};
for (int64_t i = 1;;) {
OptionalPatternMatch m = find(text, pattern, I(i));
if (m.is_none) break;
i = Int64$from_int(m.index, false) + m.text.length;
List$insert(&matches, &m, I_small(0), sizeof(PatternMatch));
}
return matches;
}
typedef struct {
TextIter_t state;
Int_t i;
Text_t pattern;
} match_iter_state_t;
static OptionalPatternMatch next_match(match_iter_state_t *state) {
if (Int64$from_int(state->i, false) > state->state.stack[0].text.length) return NONE_MATCH;
OptionalPatternMatch m = find(state->state.stack[0].text, state->pattern, state->i);
if (m.is_none) // No match
state->i = I(state->state.stack[0].text.length + 1);
else state->i = Int$plus(m.index, I(MAX(1, m.text.length)));
return m;
}
static Closure_t Pattern$by_match(Text_t text, Text_t pattern) {
return (Closure_t){
.fn = (void *)next_match,
.userdata = new (match_iter_state_t, .state = NEW_TEXT_ITER_STATE(text), .i = I_small(1), .pattern = pattern),
};
}
static bool substring_match_at(TextIter_t *haystack, TextIter_t *needle, int64_t pos) {
for (int64_t i = 0; i < needle->stack[0].text.length; i++) {
if (Text$get_grapheme_fast(haystack, pos + i) != Text$get_grapheme_fast(needle, i)) return false;
}
return true;
}
static Text_t apply_backrefs(Text_t text, List_t recursive_replacements, Text_t replacement, Text_t backref_marker,
capture_t *captures) {
if (backref_marker.length == 0) return replacement;
Text_t ret = Text("");
TextIter_t replacement_state = NEW_TEXT_ITER_STATE(replacement);
TextIter_t backref_state = NEW_TEXT_ITER_STATE(backref_marker);
int32_t first_grapheme = Text$get_grapheme_fast(&backref_state, 0);
int64_t nonmatching_pos = 0;
for (int64_t pos = 0; pos < replacement.length;) {
// Optimization: quickly skip ahead to first char in the backref pattern:
while (pos + 1 < replacement.length && Text$get_grapheme_fast(&replacement_state, pos) != first_grapheme)
++pos;
// If it's not a backref marker, proceed normally
if (!substring_match_at(&replacement_state, &backref_state, pos)) {
pos += 1;
continue;
}
// For double backrefs like "@@", treat it as an escape
if (substring_match_at(&replacement_state, &backref_state, pos + backref_marker.length)) {
if (pos > nonmatching_pos) {
Text_t before_slice = Text$slice(replacement, I(nonmatching_pos + 1), I(pos));
ret = Texts(ret, before_slice);
}
ret = Texts(ret, backref_marker);
pos += 2 * backref_marker.length;
nonmatching_pos = pos;
continue;
}
int64_t after_backref = pos + backref_marker.length;
int64_t backref = parse_int(&replacement_state, &after_backref);
if (after_backref == pos + backref_marker.length) { // Not actually a backref if there's no number
pos += 1;
continue;
}
if (backref < 0 || backref >= MAX_BACKREFS)
fail_text(Texts("Invalid backref index: ", backref, " (only 0-", MAX_BACKREFS - 1, " are allowed)"));
if (!captures[backref].occupied) fail_text(Texts("There is no capture number ", backref, "!"));
if (Text$get_grapheme_fast(&replacement_state, after_backref) == ';')
after_backref += 1; // skip optional semicolon
Text_t backref_text =
Text$slice(text, I(captures[backref].index + 1), I(captures[backref].index + captures[backref].length));
if (captures[backref].recursive && recursive_replacements.length > 0)
backref_text = replace_list(backref_text, recursive_replacements, backref_marker, true);
if (pos > nonmatching_pos) {
Text_t before_slice = Text$slice(replacement, I(nonmatching_pos + 1), I(pos));
ret = Text$concat(ret, before_slice, backref_text);
} else {
ret = Text$concat(ret, backref_text);
}
pos = after_backref;
nonmatching_pos = pos;
}
if (nonmatching_pos < replacement.length) {
Text_t last_slice = Text$slice(replacement, I(nonmatching_pos + 1), I(replacement.length));
ret = Text$concat(ret, last_slice);
}
return ret;
}
static Text_t Pattern$replace(Text_t text, Text_t pattern, Text_t replacement, Text_t backref_marker, bool recursive) {
if (text.length == 0 || pattern.length == 0) return text;
Text_t ret = EMPTY_TEXT;
int32_t first_grapheme = Text$get_grapheme(pattern, 0);
bool find_first = (first_grapheme != '{' && !uc_is_property((ucs4_t)first_grapheme, UC_PROPERTY_QUOTATION_MARK)
&& !uc_is_property((ucs4_t)first_grapheme, UC_PROPERTY_PAIRED_PUNCTUATION));
Text_t entries[2] = {pattern, replacement};
List_t replacements = {
.data = entries,
.length = 1,
.stride = sizeof(entries),
};
TextIter_t text_state = NEW_TEXT_ITER_STATE(text);
int64_t nonmatching_pos = 0;
for (int64_t pos = 0; pos < text.length;) {
// Optimization: quickly skip ahead to first char in pattern:
if (find_first) {
while (pos < text.length && Text$get_grapheme_fast(&text_state, pos) != first_grapheme)
++pos;
}
capture_t captures[MAX_BACKREFS] = {};
int64_t match_len = match(text, pos, pattern, 0, captures, 1);
if (match_len < 0) {
pos += 1;
continue;
}
captures[0] = (capture_t){
.index = pos,
.length = match_len,
.occupied = true,
.recursive = false,
};
Text_t replacement_text =
apply_backrefs(text, recursive ? replacements : (List_t){}, replacement, backref_marker, captures);
if (pos > nonmatching_pos) {
Text_t before_slice = Text$slice(text, I(nonmatching_pos + 1), I(pos));
ret = Text$concat(ret, before_slice, replacement_text);
} else {
ret = Text$concat(ret, replacement_text);
}
nonmatching_pos = pos + match_len;
pos += MAX(match_len, 1);
}
if (nonmatching_pos < text.length) {
Text_t last_slice = Text$slice(text, I(nonmatching_pos + 1), I(text.length));
ret = Text$concat(ret, last_slice);
}
return ret;
}
static Text_t Pattern$trim(Text_t text, Text_t pattern, bool trim_left, bool trim_right) {
if (text.length == 0 || pattern.length == 0) return text;
int64_t first = 0, last = text.length - 1;
if (trim_left) {
int64_t match_len = match(text, 0, pattern, 0, NULL, 0);
if (match_len > 0) first = match_len;
}
if (trim_right) {
for (int64_t i = text.length - 1; i >= first; i--) {
int64_t match_len = match(text, i, pattern, 0, NULL, 0);
if (match_len > 0 && i + match_len == text.length) last = i - 1;
}
}
return Text$slice(text, I(first + 1), I(last + 1));
}
static Text_t Pattern$map(Text_t text, Text_t pattern, Closure_t fn, bool recursive) {
if (text.length == 0 || pattern.length == 0) return text;
Text_t ret = EMPTY_TEXT;
int32_t first_grapheme = Text$get_grapheme(pattern, 0);
bool find_first = (first_grapheme != '{' && !uc_is_property((ucs4_t)first_grapheme, UC_PROPERTY_QUOTATION_MARK)
&& !uc_is_property((ucs4_t)first_grapheme, UC_PROPERTY_PAIRED_PUNCTUATION));
TextIter_t text_state = NEW_TEXT_ITER_STATE(text);
int64_t nonmatching_pos = 0;
Text_t (*text_mapper)(PatternMatch, void *) = fn.fn;
for (int64_t pos = 0; pos < text.length; pos++) {
// Optimization: quickly skip ahead to first char in pattern:
if (find_first) {
while (pos < text.length && Text$get_grapheme_fast(&text_state, pos) != first_grapheme)
++pos;
}
capture_t captures[MAX_BACKREFS] = {};
int64_t match_len = match(text, pos, pattern, 0, captures, 0);
if (match_len < 0) continue;
PatternMatch m = {
.text = Text$slice(text, I(pos + 1), I(pos + match_len)),
.index = I(pos + 1),
.captures = {},
};
for (int i = 0; captures[i].occupied; i++) {
Text_t capture = Text$slice(text, I(captures[i].index + 1), I(captures[i].index + captures[i].length));
if (recursive) capture = Pattern$map(capture, pattern, fn, recursive);
List$insert(&m.captures, &capture, I(0), sizeof(Text_t));
}
Text_t replacement = text_mapper(m, fn.userdata);
if (pos > nonmatching_pos) {
Text_t before_slice = Text$slice(text, I(nonmatching_pos + 1), I(pos));
ret = Text$concat(ret, before_slice, replacement);
} else {
ret = Text$concat(ret, replacement);
}
nonmatching_pos = pos + match_len;
pos += (match_len - 1);
}
if (nonmatching_pos < text.length) {
Text_t last_slice = Text$slice(text, I(nonmatching_pos + 1), I(text.length));
ret = Text$concat(ret, last_slice);
}
return ret;
}
static void Pattern$each(Text_t text, Text_t pattern, Closure_t fn, bool recursive) {
if (text.length == 0 || pattern.length == 0) return;
int32_t first_grapheme = Text$get_grapheme(pattern, 0);
bool find_first = (first_grapheme != '{' && !uc_is_property((ucs4_t)first_grapheme, UC_PROPERTY_QUOTATION_MARK)
&& !uc_is_property((ucs4_t)first_grapheme, UC_PROPERTY_PAIRED_PUNCTUATION));
TextIter_t text_state = NEW_TEXT_ITER_STATE(text);
void (*action)(PatternMatch, void *) = fn.fn;
for (int64_t pos = 0; pos < text.length; pos++) {
// Optimization: quickly skip ahead to first char in pattern:
if (find_first) {
while (pos < text.length && Text$get_grapheme_fast(&text_state, pos) != first_grapheme)
++pos;
}
capture_t captures[MAX_BACKREFS] = {};
int64_t match_len = match(text, pos, pattern, 0, captures, 0);
if (match_len < 0) continue;
PatternMatch m = {
.text = Text$slice(text, I(pos + 1), I(pos + match_len)),
.index = I(pos + 1),
.captures = {},
};
for (int i = 0; captures[i].occupied; i++) {
Text_t capture = Text$slice(text, I(captures[i].index + 1), I(captures[i].index + captures[i].length));
if (recursive) Pattern$each(capture, pattern, fn, recursive);
List$insert(&m.captures, &capture, I(0), sizeof(Text_t));
}
action(m, fn.userdata);
pos += (match_len - 1);
}
}
Text_t replace_list(Text_t text, List_t replacements, Text_t backref_marker, bool recursive) {
if (text.length == 0 || replacements.length == 0) return text;
Text_t ret = EMPTY_TEXT;
int64_t nonmatch_pos = 0;
for (int64_t pos = 0; pos < text.length;) {
// Find the first matching pattern at this position:
for (int64_t i = 0; i < replacements.length; i++) {
Text_t pattern = *(Text_t *)(replacements.data + i * replacements.stride);
capture_t captures[MAX_BACKREFS] = {};
int64_t len = match(text, pos, pattern, 0, captures, 1);
if (len < 0) continue;
captures[0].index = pos;
captures[0].length = len;
// If we skipped over some non-matching text before finding a match,
// insert it here:
if (pos > nonmatch_pos) {
Text_t before_slice = Text$slice(text, I(nonmatch_pos + 1), I(pos));
ret = Text$concat(ret, before_slice);
}
// Concatenate the replacement:
Text_t replacement = *(Text_t *)(replacements.data + i * replacements.stride + sizeof(Text_t));
Text_t replacement_text =
apply_backrefs(text, recursive ? replacements : (List_t){}, replacement, backref_marker, captures);
ret = Text$concat(ret, replacement_text);
pos += MAX(len, 1);
nonmatch_pos = pos;
goto next_pos;
}
pos += 1;
next_pos:
continue;
}
if (nonmatch_pos <= text.length) {
Text_t last_slice = Text$slice(text, I(nonmatch_pos + 1), I(text.length));
ret = Text$concat(ret, last_slice);
}
return ret;
}
static Text_t Pattern$replace_all(Text_t text, Table_t replacements, Text_t backref_marker, bool recursive) {
return replace_list(text, replacements.entries, backref_marker, recursive);
}
static List_t Pattern$split(Text_t text, Text_t pattern) {
if (text.length == 0) // special case
return EMPTY_LIST;
if (pattern.length == 0) // special case
return Text$clusters(text);
List_t chunks = {};
int64_t i = 0;
for (;;) {
int64_t len = 0;
int64_t found = _find(text, pattern, i, text.length - 1, &len, NULL);
if (found == i && len == 0) found = _find(text, pattern, i + 1, text.length - 1, &len, NULL);
if (found < 0) break;
Text_t chunk = Text$slice(text, I(i + 1), I(found));
List$insert(&chunks, &chunk, I_small(0), sizeof(Text_t));
i = MAX(found + len, i + 1);
}
Text_t last_chunk = Text$slice(text, I(i + 1), I(text.length));
List$insert(&chunks, &last_chunk, I_small(0), sizeof(Text_t));
return chunks;
}
typedef struct {
TextIter_t state;
int64_t i;
Text_t pattern;
} split_iter_state_t;
static OptionalText_t next_split(split_iter_state_t *state) {
Text_t text = state->state.stack[0].text;
if (state->i >= text.length) {
if (state->pattern.length > 0 && state->i == text.length) { // special case
state->i = text.length + 1;
return EMPTY_TEXT;
}
return NONE_TEXT;
}
if (state->pattern.length == 0) { // special case
Text_t ret = Text$cluster(text, I(state->i + 1));
state->i += 1;
return ret;
}
int64_t start = state->i;
int64_t len = 0;
int64_t found = _find(text, state->pattern, start, text.length - 1, &len, NULL);
if (found == start && len == 0) found = _find(text, state->pattern, start + 1, text.length - 1, &len, NULL);
if (found >= 0) {
state->i = MAX(found + len, state->i + 1);
return Text$slice(text, I(start + 1), I(found));
} else {
state->i = state->state.stack[0].text.length + 1;
return Text$slice(text, I(start + 1), I(text.length));
}
}
static Closure_t Pattern$by_split(Text_t text, Text_t pattern) {
return (Closure_t){
.fn = (void *)next_split,
.userdata = new (split_iter_state_t, .state = NEW_TEXT_ITER_STATE(text), .i = 0, .pattern = pattern),
};
}
static Text_t Pattern$escape_text(Text_t text) {
// TODO: optimize for spans of non-escaped text
Text_t ret = EMPTY_TEXT;
TextIter_t state = NEW_TEXT_ITER_STATE(text);
for (int64_t i = 0; i < text.length; i++) {
uint32_t g = Text$get_main_grapheme_fast(&state, i);
if (g == '{') {
ret = Text$concat(ret, Text("{1{}"));
} else if (g == '?' || uc_is_property_quotation_mark(g)
|| (uc_is_property_paired_punctuation(g) && uc_is_property_left_of_pair(g))) {
ret = Text$concat(ret, Text("{1"), Text$slice(text, I(i + 1), I(i + 1)), Text("}"));
} else {
ret = Text$concat(ret, Text$slice(text, I(i + 1), I(i + 1)));
}
}
return ret;
}
static Text_t Pattern$as_text(const void *obj, bool colorize, const TypeInfo_t *info) {
(void)info;
if (!obj) return Text("Pattern");
Text_t pat = *(Text_t *)obj;
Text_t quote = Pattern$has(pat, Text("/")) && !Pattern$has(pat, Text("|")) ? Text("|") : Text("/");
return Text$concat(colorize ? Text("\x1b[1m$\033[m") : Text("$"), Text$quoted(pat, colorize, quote));
}
|