-- @titre Reprendre la revue de Fontista Control -- @title Bring in the Fontista Control review -- @categorie relire -- @resume Lit la dernière revue exportée par Fontista Control (ou le fichier JSON indiqué) : les glyphes à corriger passent en rouge, avec la note et la position des repères dans leur note de glyphe ; pour une revue des liaisons, les deux lettres de chaque paire. Édition du site seulement (la version App Store ne lit pas les fichiers des autres apps). -- @summary Reads the latest review exported by Fontista Control (or the JSON file you name): glyphs to fix turn red, with the note and the mark positions in their glyph note; for a join review, both letters of each pair. Site edition only (the App Store version cannot read other apps' files). -- Fontista · bibliothèque de scripts · https://fontista.thinkspark.dev/scripts.html local FILE = "" -- empty: the latest export found; or the full path of a review .json local RED = 0 -- colour label for glyphs to fix local GREEN = nil -- colour label for approved glyphs (3 = green), nil to leave them local en = lang == "en" local windows = package.config:sub(1, 1) == "\\" -- latest review: Control keeps a copy of every export in its own folder; Downloads is searched too local function latest() local home = os.getenv("HOME") or os.getenv("USERPROFILE") or "" local dirs if windows then dirs = { (os.getenv("APPDATA") or "") .. "\\Fontista Control\\exports", home .. "\\Downloads" } elseif io.open("/System/Library", "r") then dirs = { home .. "/Library/Application Support/Fontista Control/exports", home .. "/Downloads" } else dirs = { (os.getenv("XDG_DATA_HOME") or (home .. "/.local/share")) .. "/Fontista Control/exports", home .. "/Downloads" } end local best, best_t = nil, nil for _, d in ipairs(dirs) do local cmd = windows and ('dir /b /o-d "' .. d .. '\\*.json" 2>nul') or ('ls -t "' .. d .. '"/*.json 2>/dev/null') local p = io.popen(cmd) if p then for line in p:lines() do local f = windows and (d .. "\\" .. line) or line local h = io.open(f, "r") local head = h and h:read(400) or "" if h then h:close() end if head:find('"statuts"', 1, true) or head:find('"police"', 1, true) then -- newest file of each folder comes first; compare folders by modification time when possible local t = nil if not windows then local s = io.popen('stat -f %m "' .. f .. '" 2>/dev/null || stat -c %Y "' .. f .. '" 2>/dev/null') t = s and tonumber(s:read("l") or "") or nil if s then s:close() end end if not best or (t and best_t and t > best_t) then best, best_t = f, t end break end end p:close() end end return best end -- small JSON reader (objects, arrays, strings, numbers, true, false, null) local function decode(s) local i = 1 local function ws() i = s:find("[^ \t\r\n]", i) or #s + 1 end local function str() local out, j = {}, i + 1 while true do local c = s:sub(j, j) if c == '"' then i = j + 1 return table.concat(out) end if c == "\\" then local e = s:sub(j + 1, j + 1) if e == "u" then local cp = tonumber(s:sub(j + 2, j + 5), 16) j = j + 6 if cp >= 0xD800 and cp <= 0xDBFF and s:sub(j, j + 1) == "\\u" then cp = 0x10000 + (cp - 0xD800) * 0x400 + (tonumber(s:sub(j + 2, j + 5), 16) - 0xDC00) j = j + 6 end out[#out + 1] = utf8.char(cp) else out[#out + 1] = ({ b = "\b", f = "\f", n = "\n", r = "\r", t = "\t" })[e] or e j = j + 2 end elseif c == "" then error("JSON: unterminated string") else out[#out + 1] = c j = j + 1 end end end local value function value() ws() local c = s:sub(i, i) if c == "{" then local t = {} i = i + 1 ws() if s:sub(i, i) == "}" then i = i + 1 return t end while true do ws() local k = str() ws() i = i + 1 t[k] = value() ws() local d = s:sub(i, i) i = i + 1 if d == "}" then return t end end elseif c == "[" then local t = {} i = i + 1 ws() if s:sub(i, i) == "]" then i = i + 1 return t end while true do t[#t + 1] = value() ws() local d = s:sub(i, i) i = i + 1 if d == "]" then return t end end elseif c == '"' then return str() elseif s:sub(i, i + 3) == "true" then i = i + 4 return true elseif s:sub(i, i + 4) == "false" then i = i + 5 return false elseif s:sub(i, i + 3) == "null" then i = i + 4 return nil else local num = s:match("^-?%d+%.?%d*[eE]?[-+]?%d*", i) if not num or num == "" then error("JSON: unexpected character at " .. i) end i = i + #num return tonumber(num) end end return value() end local path = FILE ~= "" and FILE or latest() if not path then print(en and "No Fontista Control review found. Export one (File › Export the review), or set FILE at the top of this script." or "Aucune revue de Fontista Control trouvée. Exportez-en une (Fichier › Exporter la revue), ou indiquez FILE en tête de ce script.") return end local h = assert(io.open(path, "r")) local review = decode(h:read("a")) h:close() local fixed, approved, missing = 0, 0, {} local function flag(g, text) if not glyph(g) then missing[#missing + 1] = g return end set_mark(g, RED) -- keep the designer's own note, replace a previous Control line local note = (glyph(g).note or ""):gsub("\n?Control : [^\n]*", ""):gsub("\n?Control: [^\n]*", "") set_note(g, (note ~= "" and (note .. "\n") or "") .. (en and "Control: " or "Control : ") .. text) fixed = fixed + 1 end for _, item in ipairs(review.liste_a_corriger or {}) do local text = item.note ~= "" and item.note or (en and "to fix" or "à corriger") if item.mesure and item.mesure ~= "" then text = text .. " (" .. item.mesure .. ")" end local marks = {} for _, r in ipairs(item.reperes or {}) do marks[#marks + 1] = string.format("%.0f;%.0f%s", r.x or 0, r.y or 0, (r.note and r.note ~= "") and (" " .. r.note) or "") end if #marks > 0 then text = text .. (en and " · marks " or " · repères ") .. table.concat(marks, ", ") end if review.liaisons then for _, g in ipairs(item.glyphes or {}) do flag(g, item.nom .. " : " .. text) end else flag(item.nom, text) end end if GREEN and not review.liaisons then for name, st in pairs(review.statuts or {}) do if st == "ok" and glyph(name) then set_mark(name, GREEN) approved = approved + 1 end end end print(string.format(en and "%s · pass %s · %d glyph(s) to fix marked%s" or "%s · passe %s · %d glyphe(s) à corriger marqué(s)%s", path, tostring(review.passe or "?"), fixed, GREEN and string.format(en and ", %d approved" or ", %d validé(s)", approved) or "")) if #missing > 0 then print((en and "Not in this font: " or "Absents de cette fonte : ") .. table.concat(missing, ", ")) end