Form view: structured key/value editor (keys read-only, values editable; side-by-side desktop, stacked mobile) + testable strings-format.js
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* FuelBoard Copy Editor — .strings format helpers (pure, no DOM).
|
||||
*
|
||||
* The editor can show Localizable.strings either as raw text or as a
|
||||
* structured key/value form. These functions parse the file into blocks
|
||||
* (entries + comments + blanks) and serialize them back, preserving every
|
||||
* comment and blank line byte-for-byte.
|
||||
*
|
||||
* .strings shape (verified against the real file):
|
||||
* "Key" = "Value"; -> entry (single line)
|
||||
* /* Section banner *\/ -> comment
|
||||
* (blank) -> blank
|
||||
* Multi-line comment blocks (like the header) are kept verbatim. Any line
|
||||
* that is not a valid entry, comment, or blank is kept verbatim as "raw"
|
||||
* and the form view refuses to activate (falls back to Raw editing) so we
|
||||
* can never clobber a file we don't understand.
|
||||
*/
|
||||
(function (root, factory) {
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = factory();
|
||||
} else {
|
||||
root.StringsFormat = factory();
|
||||
}
|
||||
})(this, function () {
|
||||
"use strict";
|
||||
|
||||
// Display value -> file-escaped value. Backslash FIRST, then quotes.
|
||||
function escapeStr(s) {
|
||||
return s
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, "\\n")
|
||||
.replace(/\t/g, "\\t");
|
||||
}
|
||||
|
||||
// File-escaped value -> display value. Unknown escapes (\uXXXX, \') are
|
||||
// passed through untouched so round-trips never lose data.
|
||||
function unescapeStr(s) {
|
||||
return s.replace(/\\(.)/g, function (m, c) {
|
||||
return c === "n" ? "\n" : c === "t" ? "\t" : c === '"' ? '"' : c === "\\" ? "\\" : m;
|
||||
});
|
||||
}
|
||||
|
||||
// Text -> [{type:"blank"|"comment"|"entry"|"raw", ...}]
|
||||
// entry: {type, key, value (unescaped), raw}
|
||||
function parseStrings(text) {
|
||||
var lines = text.split("\n");
|
||||
var blocks = [];
|
||||
var i = 0;
|
||||
while (i < lines.length) {
|
||||
var raw = lines[i];
|
||||
var s = raw.trim();
|
||||
if (s === "") {
|
||||
blocks.push({ type: "blank", raw: "" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (s.indexOf("/*") === 0) {
|
||||
// Comment block — may span lines (the file header does).
|
||||
var block = [raw];
|
||||
i++;
|
||||
while (i < lines.length && block[block.length - 1].indexOf("*/") === -1) {
|
||||
block.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
blocks.push({ type: "comment", raw: block.join("\n") });
|
||||
continue;
|
||||
}
|
||||
var m = s.match(/^"((?:[^"\\]|\\.)*)"\s*=\s*"((?:[^"\\]|\\.)*)"\s*;$/);
|
||||
if (m) {
|
||||
blocks.push({ type: "entry", key: m[1], value: unescapeStr(m[2]), raw: raw });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
// Not something we understand — keep verbatim, flag for the caller.
|
||||
blocks.push({ type: "raw", raw: raw });
|
||||
i++;
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function serializeStrings(blocks) {
|
||||
return blocks
|
||||
.map(function (b) {
|
||||
if (b.type === "entry") {
|
||||
return '"' + b.key + '" = "' + escapeStr(b.value) + '";';
|
||||
}
|
||||
return b.raw;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
return { parseStrings: parseStrings, serializeStrings: serializeStrings, escapeStr: escapeStr, unescapeStr: unescapeStr };
|
||||
});
|
||||
Reference in New Issue
Block a user