diff --git a/format/desktop_entry/desktop_entry.ha b/format/desktop_entry/desktop_entry.ha index 3aaed68..2fc17a7 100644 --- a/format/desktop_entry/desktop_entry.ha +++ b/format/desktop_entry/desktop_entry.ha @@ -1,20 +1,6 @@ use locale; use strings; -// Represents a file of the generic key/value format used by desktop entries. -// Specification: §3 -export type file = struct { - preceeding_lines: []line, - groups: []group, -}; - -// A named group of key/value entries. -// Specification: §3.2 -export type group = struct { - name: str, - lines: []line, -}; - // A line in the file, which can be a comment (or a blank line), an entry, or // a localized entry. export type line = (blank | comment | entry | localized_entry); @@ -27,195 +13,39 @@ export type blank = void; // Specification: §3.1 export type comment = str; +// A group header. +// Specification: §3.2 +export type group_header = str; + // A key/value pair. // Specification: §3.3 -export type entry = struct { - key: str, - value: str, -}; +export type entry = (str, str); // A localized key/value pair. // Specification: §5 -export type localized_entry = struct { - key: str, - value: str, - locale: locale::locale, +export type localized_entry = (str, str, locale::locale); + +// Duplicates an [[entry]]. Use [[entry_finish]] to get rid of it. +export fn entry_dup(entr: entry) entry = ( + strings::dup(entr.0), + strings::dup(entr.1)); + +// Frees memory associated with an [[entry]]. +export fn entry_finish(entr: entry) void = { + free(entr.0); + free(entr.1); }; -// Gets a non-localized [[value]] from a [[file]]. If the group does not exist, -// or the group exists but the key isn't in it, it returns void. -export fn file_get(fil: *file, group_name: str, key: str) (str | void) = { - let grou = match (file_find_group(fil, group_name)) { - case let index: size => yield fil.groups[index]; - case void => return void; - }; - let lin = match (group_find_entry(grou, key)) { - case let index: size => yield grou.lines[index]; - case void => return void; - }; - match (lin) { - case let entr: entry => return entr.value; - case => abort(); - }; -}; - -// Gets a localized [[value]] from a [[file]]. If the group does not exist, or -// the group exists but the key isnt in it, it returns void. If the key is in -// the group but is not localized to the specified [[locale::locale]], the -// non-localized value is returned. Refer to the specification for the exact way -// which the locale is matched. -// Specification: §5 -export fn file_get_localized( - fil: *file, - group_name: str, - key: str, - local: locale::locale, -) (str | void) = { - let grou = match (file_find_group(fil, group_name)) { - case let index: size => yield fil.groups[index]; - case void => return void; - }; - let lin = match (group_find_localized_entry(grou, key, local)) { - case let index: size => yield grou.lines[index]; - case void => - yield match(group_find_entry(grou, key)) { - case let index: size => yield index; - case void => return void; - }; - }; - match (lin) { - case let entr: localized_entry => return entr.value; - case let entr: entry => return entr.value; - case => abort(); - }; -}; - -// TODO - -//export fn file_add - -//export fn file_add_localized - -//export fn file_remove - -//export fn file_remove_localized - -//export fn file_encode - -// Frees memory associated with a [[file]]. -export fn file_finish(fil: *file) void = { - for (let lin .. fil.preceeding_lines) { - line_finish(lin); - }; - free(fil.preceeding_lines); - for (let grou .. fil.groups) { - group_finish(grou); - }; - free(fil.groups); -}; - -// Frees memory associated with a [[line]]. -export fn line_finish(lin: line) void = match (lin) { -case let lin: comment => - free(lin); -case let lin: entry => - free(lin.key); - free(lin.value); -case let lin: localized_entry => - free(lin.key); - free(lin.value); - locale::finish(lin.locale); -case => void; -}; - -// Frees memory associated with a [[group]]. -export fn group_finish(grou: group) void = { - for (let lin .. grou.lines) { - line_finish(lin); - }; - free(grou.lines); -}; - -fn file_find_group(fil: *file, group_name: str) (size | void) = { - let index = 0z; - for (let grou .. fil.groups) { - if (grou.name == group_name) { - return index; - }; - index += 1; - }; -}; - -fn group_find_entry(grou: group, key: str) (size | void) = { - let index = 0z; - for (let lin .. grou.lines) { - match (lin) { - case let entr: entry => - if (entr.key == key) { - return index; - }; - case => void; - }; - index += 1; - }; -}; - -fn group_find_localized_entry(grou: group, key: str, local: locale::locale) (size | void) = { - // The matching is done as follows. If LC_MESSAGES is of the form - // lang_COUNTRY.ENCODING@MODIFIER, then it will match a key of the form - // lang_COUNTRY@MODIFIER. If such a key does not exist, it will attempt - // to match lang_COUNTRY followed by lang@MODIFIER. Then, a match - // against lang by itself will be attempted. Finally, if no matching - // key is found the required key without a locale specified is used. - // The encoding from the LC_MESSAGES value is ignored when matching. - // - // If LC_MESSAGES does not have a MODIFIER field, then no key with a - // modifier will be matched. Similarly, if LC_MESSAGES does not have a - // COUNTRY field, then no key with a country specified will be matched. - // If LC_MESSAGES just has a lang field, then it will do a straight - // match to a key with a similar value. - - let lang_country_modifier = local; - lang_country_modifier.encoding = ""; - match(group_find_localized_entry_exact(grou, key, lang_country_modifier)) { - case let index: size => return index; - case => void; - }; - - let lang_country = lang_country_modifier; - lang_country.modifier = ""; - match(group_find_localized_entry_exact(grou, key, lang_country)) { - case let index: size => return index; - case => void; - }; - - let lang_modifier = lang_country_modifier; - lang_modifier.country = ""; - match(group_find_localized_entry_exact(grou, key, lang_modifier)) { - case let index: size => return index; - case => void; - }; - - let lang = lang_modifier; - lang.modifier = ""; - match(group_find_localized_entry_exact(grou, key, lang)) { - case let index: size => return index; - case => void; - }; - - return void; -}; - -fn group_find_localized_entry_exact(grou: group, key: str, local: locale::locale) (size | void) = { - let index = 0z; - for (let lin .. grou.lines) { - match (lin) { - case let entr: localized_entry => - if (entr.key == key && locale::equal(entr.locale, local)) { - return index; - }; - case => void; - }; - index += 1; - }; +// Duplicates a [[localized_entry]]. Use [[localized_entry_finish]] to get rid +// of it. +export fn localized_entry_dup(entr: localized_entry) localized_entry = ( + strings::dup(entr.0), + strings::dup(entr.1), + locale::dup(entr.2)); + +// Frees memory associated with an [[localized_entry]]. +export fn localized_entry_finish(entr: localized_entry) void = { + free(entr.0); + free(entr.1); + locale::finish(entr.2); }; diff --git a/format/desktop_entry/error.ha b/format/desktop_entry/error.ha index f59784d..fc5e349 100644 --- a/format/desktop_entry/error.ha +++ b/format/desktop_entry/error.ha @@ -1,5 +1,11 @@ -// All errors defined in this module. -export type error = !( +use encoding::utf8; +use io; + +// Any error that may occur during parsing. +export type error = !(syntaxerr | io::error | utf8::invalid); + +// All syntax errors defined in this module. +export type syntaxerr = !( invalid_group_header | invalid_entry | invalid_ascii | @@ -20,8 +26,10 @@ export type invalid_escape = !void; // Converts a desktop entry [[error]] into a user-friendly string. export fn strerror(err: error) str = match (err) { -case invalid_group_header => yield "invalid group header"; -case invalid_entry => yield "invalid entry"; -case invalid_ascii => yield "invalid ascii"; -case invalid_escape => yield "invalid escape"; +case invalid_group_header => yield "invalid group header"; +case invalid_entry => yield "invalid entry"; +case invalid_ascii => yield "invalid ascii"; +case invalid_escape => yield "invalid escape"; +case let err: io::error => yield io::strerror(err); +case let err: utf8::invalid => yield utf8::strerror(err); }; diff --git a/format/desktop_entry/parse.ha b/format/desktop_entry/parse.ha index 2439335..e856138 100644 --- a/format/desktop_entry/parse.ha +++ b/format/desktop_entry/parse.ha @@ -6,83 +6,69 @@ use locale; use memio; use strings; -// Parses a [[file]]. The result must be freed using [[file_finish]]. -export fn file_parse(in: io::stream) !(file | error | io::error | utf8::invalid) = { - let scanner = bufio::newscanner(&in); - defer bufio::finish(&scanner); +export type scanner = struct { + scanner: bufio::scanner, + entry: entry, + localized_entry: localized_entry, +}; - let fil = file { ... }; +// Creates a desktop entry file scanner. Use [[next]] to read lines. The caller +// must call [[finish]] once they're done with this object. +export fn scan(input: io::handle) scanner = scanner { + scanner = bufio::newscanner(input), + ... +}; + +// Returns the next entry from a desktop entry file. The return value is +// borrowed from the [[scanner]] use [[line_dup]] to retain a copy. +export fn next(this: *scanner) (line | io::EOF | error) = { + let text = match (bufio::scan_line(&this.scanner)) { + case let text: const str => + yield text; + case let err: (io::error | utf8::invalid) => + return err; + case io::EOF => + return io::EOF; + }; - for (true) { - let text = match (bufio::scan_line(&scanner)) { - case let text: const str => - yield text; - case let err: io::EOF => - break; - case let err: (io::error | utf8::invalid) => - file_finish(&fil); - return err; - }; + if (text == "") { + // blank line + return blank; - if (text == "") { - // blank line - file_append_line(&fil, blank); - - } else if (strings::hasprefix(text, '#')) { - // comment - let text = strings::dup(strings::ltrim(text, '#')); - file_append_line(&fil, text: comment); - - } else if (strings::hasprefix(text, '[')) { - // group header - let name = strings::dup(parse_group_header(text)?); - file_append_group(&fil, name); - - } else { - // key/value pair - let (key, valu, local) = match (parse_entry(text)) { - case let result: (str, str, (locale::locale | void)) => - yield result; - case let err: error => - file_finish(&fil); - return err; - }; - - file_append_line(&fil, match (local) { - case let local: locale::locale => - yield localized_entry { - key = key, - value = valu, - locale = local, - }; - case void => - yield entry { - key = key, - value = valu, - }; - }); + } else if (strings::hasprefix(text, '#')) { + // comment + return strings::ltrim(text, '#'): comment; + + } else if (strings::hasprefix(text, '[')) { + // group header + return parse_group_header(text)?; + + } else { + // key/value pair + let (key, valu, local) = parse_entry(text)?; + + match (local) { + case let local: locale::locale => + localized_entry_finish(this.localized_entry); + this.localized_entry = (key, valu, local); + return this.localized_entry; + case void => + entry_finish(this.entry); + this.entry = (key, valu); + return this.entry; }; }; - return fil; + }; -fn file_append_group(fil: *file, name: str) void = { - append(fil.groups, group { - name = name, - ... - }); -}; - -fn file_append_line(fil: *file, lin: line) void = { - if (len(fil.groups) > 0) { - append(fil.groups[len(fil.groups) - 1].lines, lin); - } else { - append(fil.preceeding_lines, lin); - }; +// Frees resources associated with a [[scanner]]. +export fn finish(this: *scanner) void = { + bufio::finish(&this.scanner); + entry_finish(this.entry); }; // memory is borrowed from the input -fn parse_group_header(text: str) (str | error) = { +fn parse_group_header(text: str) (group_header | error) = { if (!strings::hasprefix(text, '[') || !strings::hassuffix(text, ']')) { return invalid_group_header; }; @@ -91,7 +77,7 @@ fn parse_group_header(text: str) (str | error) = { if (strings::contains(text, '[', ']')) { return invalid_group_header; }; - return text; + return text: group_header; }; // memory must be freed by the caller