local V = require("texecole-vocab") local P = {} local function trim(s) return (s:gsub("^%s+", ""):gsub("%s+$", "")) end local function num_fr(s) return (s:gsub("(%d),(%d)", "%1.%2")) end local function coords_fr(s) s = num_fr(s) return (s:gsub("%s*;%s*", ",")) end local function split_fragments(s) local frags, buf, depth = {}, {}, 0 local i, n = 1, #s while i <= n do local c = s:sub(i, i) if c == "{" or c == "(" or c == "[" then depth = depth + 1 elseif c == "}" or c == ")" or c == "]" then depth = depth - 1 end if depth == 0 and c == "," and s:sub(i + 1, i + 1) == " " then frags[#frags + 1] = trim(table.concat(buf)); buf = {} i = i + 2 else buf[#buf + 1] = c i = i + 1 end end if #buf > 0 then frags[#frags + 1] = trim(table.concat(buf)) end return frags end local function split_et(frag) local parts, buf, depth = {}, {}, 0 for word in frag:gmatch("%S+") do if word:find("[{(]") then depth = depth + 1 end if word:find("[})]") then depth = depth - 1 end if depth == 0 and word == "et" then parts[#parts + 1] = trim(table.concat(buf, " ")); buf = {} else buf[#buf + 1] = word end end if #buf > 0 then parts[#parts + 1] = trim(table.concat(buf, " ")) end return parts end local function err(msg) error("texecole : " .. msg, 0) end local SPECIES = { ["triangle"] = "triangle", ["carré"] = "square", ["rectangle"] = "rectangle", ["losange"] = "rhombus", ["trapèze"] = "trapezoid", ["parallélogramme"] = "parallelogram", ["pentagone"] = "pentagon", ["hexagone"] = "hexagon", ["octogone"] = "octagon", ["polygone"] = "polygon", ["cerf-volant"] = "kite", ["cercle"] = "circle", ["disque"] = "circle", ["point"] = "point", ["vecteur"] = "vector", ["région"] = "region", ["tangente"] = "tangent", } local POLYGON_BY_N = { [5] = "pentagon", [6] = "hexagon", [7] = "heptagon", [8] = "octagon", } local SOLIDS = { ["cube"] = "cube", ["pavé"] = "cuboid", ["pyramide"] = "pyramid", ["cylindre"] = "cylinder", ["cône"] = "cone", ["sphère"] = "sphere", } local function key_fr(key) local FR = { side = "côté", radius = "rayon", diameter = "diamètre", base = "base", height = "hauteur", edge = "arête", scale = "coefficient" } return FR[key] or key end local function len_attr(key) return function(rest, out, ctx) local seg, val, unit = rest:match("^(%u%u)%s+([%d,%.]+)%s*(c?m?m?)$") if seg and key == "side" then if unit == "" and not (ctx and ctx.frame) then err("longueur sans unité : « " .. rest .. " ». Écrivez par exemple " .. "« " .. seg .. " " .. val .. " cm » — l'unité (cm ou mm) est " .. "obligatoire hors d'un repère.") end out[#out + 1] = seg .. ":" .. num_fr(val) if unit ~= "" then out.measures = unit end return true end local segonly = rest:match("^(%u%u)$") if segonly then out[#out + 1] = key .. ":" .. segonly; return true end local v, u = rest:match("^([%d,%.]+)%s*(c?m?m?)$") if v then if u == "" and not (ctx and ctx.frame) then err("longueur sans unité : « de " .. key_fr(key) .. " " .. v .. " ». Écrivez « de " .. key_fr(key) .. " " .. v .. " cm » ou « " .. v .. " mm » — l'unité est obligatoire hors d'un repère, où " .. "seules les graduations dispensent d'unité.") end out[#out + 1] = key .. ":" .. num_fr(v) if u ~= "" then out.measures = u end return true end return false end end local DE_ATTRS = { { pref = "de côté", f = len_attr("side") }, { pref = "de rayon", f = len_attr("radius") }, { pref = "de diamètre", f = len_attr("diameter") }, { pref = "de base", f = len_attr("base") }, { pref = "de hauteur", f = len_attr("height") }, { pref = "d'arête", f = len_attr("edge") }, { pref = "d'angle", f = function(rest, out) local v = rest:match("^([%d,%.]+)$") if v then out[#out + 1] = "angle:" .. num_fr(v); return true end return false end }, { pref = "de coefficient", f = len_attr("scale") }, { pref = "de centre", f = function(rest, out) local co = rest:match("^(%b())$") if co then out[#out + 1] = "center:" .. coords_fr(co); return true end local nm = rest:match("^(%u[%w]*)$") if nm then out[#out + 1] = "center:" .. nm; return true end return false end }, { pref = "d'intervalle", f = function(rest, out) local b = rest:match("^(%b{})$") if b then out[#out + 1] = "set:" .. num_fr(b); return true end return false end }, { pref = "de points", f = function(rest, out) local b = rest:match("^(%b{})$") if b then out[#out + 1] = "points:" .. num_fr(b:gsub("%s*;%s*", ", ")); return true end return false end }, { pref = "de trait", f = function(rest, out) local v = rest:match("^([%d,%.]+)%s*mm$") if v then out[#out + 1] = "thickness:" .. num_fr(v); return true end if rest:match("^[%d,%.]+$") then err("l'épaisseur du trait s'écrit avec son unité, en millimètres : " .. "« de trait " .. rest .. " mm » — l'unité est obligatoire, " .. "comme pour toute mesure (règle des unités).") end return false end }, } local AVEC_SANS = { ["marques"] = "marks", ["valeurs"] = "values", ["labels"] = "labels", ["mesures"] = "measures", ["ajustement"] = "fit", ["produits"] = "products", } local function sans_article(w) return w:match("^les%s+(.+)$") or w:match("^le%s+(.+)$") or w:match("^la%s+(.+)$") or w:match("^l'%s*(.+)$") or w end function P.parse_bounds(frag) local function intervalle(s) return s:match("%[%s*([%-%d,%.]+)%s*;%s*([%-%d,%.]+)%s*%]") end local xzone = frag:match("^pour%s+x%s+appartient%s+à%s*(%b[])") or frag:match("^pour%s+x%s+dans%s*(%b[])") or frag:match("^pour%s+x%s+à%s*(%b[])") if xzone then local ax, bx = intervalle(xzone) local reste = frag:match("et%s+y%s+appartient%s+à%s*(%b[])") or frag:match("et%s+y%s+dans%s*(%b[])") or frag:match("et%s+y%s+à%s*(%b[])") local ay, by if reste then ay, by = intervalle(reste) end return num_fr(ax), num_fr(bx), ay and num_fr(ay), by and num_fr(by) end local sa, sb = frag:match("^sur%s*%[%s*([%-%d,%.]+)%s*;%s*([%-%d,%.]+)%s*%]$") if sa then return num_fr(sa), num_fr(sb), nil, nil end local oa, ob = frag:match("^en%s+ordonnée%s*%[%s*([%-%d,%.]+)%s*;%s*([%-%d,%.]+)%s*%]$") if oa then return nil, nil, num_fr(oa), num_fr(ob) end return nil end local function unite_en_cm(val, unit) local n = tonumber(num_fr(val)) if not n then return nil end if unit == "mm" then n = n / 10 end return (string.format("%.4f", n):gsub("0+$", ""):gsub("%.$", "")) end local function translate_fragment(frag, out, ctx) local x1, x2, y1, y2 = P.parse_bounds(frag) if x1 or y1 then if x1 then out.x = "{" .. x1 .. ", " .. x2 .. "}" end if y1 then out.y = "{" .. y1 .. ", " .. y2 .. "}" end return true end -- Unité du repère, en prose (règle des unités : cm ou mm obligatoire). -- « avec une unité de 1,5 cm » : la même sur les deux axes (orthonormé) ; -- « avec des unités respectives de 2 cm et 0,5 cm » : abscisses puis -- ordonnées (distribution respective), le repère est alors orthogonal. do local uv, uu = frag:match("^avec%s+une%s+unité%s+de%s+([%d,%.]+)%s*([cm]m)$") if uv then local u = unite_en_cm(uv, uu) out.unitx, out.unity = u, u out[#out + 1] = "unitx:" .. u out[#out + 1] = "unity:" .. u return true end local xv, xu, yv, yu = frag:match( "^avec%s+des%s+unités%s+respectives%s+de%s+([%d,%.]+)%s*([cm]m)%s+et%s+([%d,%.]+)%s*([cm]m)$") if not xv then xv, xu, yv, yu = frag:match( "^avec%s+des%s+unités%s+respectives%s+de%s+([%d,%.]+)%s*([cm]m)%s+et%s+de%s+([%d,%.]+)%s*([cm]m)$") end if not xv then -- Forme académique, chaque axe nommé en toutes lettres : -- « avec des unités graphiques de 1,5 cm pour l'axe des abscisses -- et de 0,5 cm pour l'axe des ordonnées » xv, xu, yv, yu = frag:match( "^avec%s+des%s+unités%s+graphiques%s+de%s+([%d,%.]+)%s*([cm]m)%s+" .. "pour%s+l'axe%s+des%s+abscisses%s+et%s+de%s+([%d,%.]+)%s*([cm]m)%s+" .. "pour%s+l'axe%s+des%s+ordonnées$") end if not xv then -- Même forme, ordonnées d'abord — les deux valeurs se remettent -- dans l'ordre abscisses puis ordonnées. local ya, yau, xa2, xau = frag:match( "^avec%s+des%s+unités%s+graphiques%s+de%s+([%d,%.]+)%s*([cm]m)%s+" .. "pour%s+l'axe%s+des%s+ordonnées%s+et%s+de%s+([%d,%.]+)%s*([cm]m)%s+" .. "pour%s+l'axe%s+des%s+abscisses$") if ya then xv, xu, yv, yu = xa2, xau, ya, yau end end if xv then out.unitx = unite_en_cm(xv, xu) out.unity = unite_en_cm(yv, yu) out[#out + 1] = "unitx:" .. out.unitx out[#out + 1] = "unity:" .. out.unity return true end if frag:match("^avec%s+une%s+unité") or frag:match("^avec%s+des%s+unités") then err("unité du repère illisible : « " .. frag .. " ». Trois formes : " .. "« avec une unité de 1,5 cm » (la même sur les deux axes), " .. "« avec des unités respectives de 2 cm et 0,5 cm », ou la " .. "forme académique « avec des unités graphiques de 2 cm pour " .. "l'axe des abscisses et de 0,5 cm pour l'axe des ordonnées ». " .. "L'unité (cm ou mm) est obligatoire sur chaque valeur.") end end local fa, fb = frag:match("^avec%s+l'aire%s+entre%s+([%-%d,%.]+)%s+et%s+([%-%d,%.]+)$") if fa then out[#out + 1] = "area:{" .. num_fr(fa) .. ", " .. num_fr(fb) .. "}" return true end for _, atom0 in ipairs(split_et(frag)) do local kvtail = {} local atom = atom0 while true do local pre, kv = atom:match("^(.-)%s+([%wéèê_]+:%S+)%s*$") if not kv then break end table.insert(kvtail, 1, kv) atom = trim(pre) end if atom == "" and #kvtail > 0 then atom = table.remove(kvtail, 1) end for _, kv in ipairs(kvtail) do local k, v = kv:match("^([%wéèê_]+):(.+)$") if k == "unité" or k == "unite" then err("le réglage « unité: » n'existe plus : l'unité du repère se " .. "dit en prose — « avec une unité de 1 cm », ou « avec des " .. "unités respectives de 2 cm et 0,5 cm » (abscisses puis " .. "ordonnées).") end v = v:gsub("^%$(.-)%$$", "{%1}") out[#out + 1] = (V.KV_KEYS[k] or k) .. ":" .. num_fr(v) end local done = false local a = atom:match("^avec%s+(.+)$") local s = atom:match("^sans%s+(.+)$") if a and AVEC_SANS[sans_article(a)] then out[#out + 1] = AVEC_SANS[sans_article(a)] .. ":on"; done = true elseif s and AVEC_SANS[sans_article(s)] then out[#out + 1] = AVEC_SANS[sans_article(s)] .. ":off"; done = true end if not done then local ech = atom:match("^avec%s+(%d+)%s+échantillons?$") if ech then out[#out + 1] = "samples:" .. ech done = true end end if not done then local aa, ab = atom:match("^avec%s+l'aire%s+entre%s+([%-%d,%.]+)%s+et%s+([%-%d,%.]+)$") if aa then out[#out + 1] = "area:{" .. num_fr(aa) .. ", " .. num_fr(ab) .. "}" done = true end end if not done then local g = atom:match("^avec%s+l'aire%s+jusqu'à%s+([%a_][%w_]*)$") if g then out[#out + 1] = "between:" .. g done = true end end if not done then -- Diagramme en escalier d'une suite récurrente (règle n°10 : -- chaque fois qu'un nom français existe, la prose gagne). local u0, n = atom:match( "^avec%s+le%s+diagramme%s+en%s+escalier%s+depuis%s+([%-%d,%.]+)" .. "%s+sur%s+(%d+)%s+termes?$") if not u0 then u0 = atom:match( "^avec%s+le%s+diagramme%s+en%s+escalier%s+depuis%s+([%-%d,%.]+)$") end if u0 then out[#out + 1] = "cobweb:{" .. num_fr(u0) .. (n and (", " .. n) or "") .. "}" done = true end end if not done then local c = atom:match("^en%s+(.+)$") if c then local svg = V.color(c) if not svg then err("couleur inconnue : « " .. c .. " ». Les 147 noms français " .. "sont listés dans la référence des couleurs du manuel.") end out[#out + 1] = "color:" .. svg done = true end end if not done and atom == "rempli" then out[#out + 1] = "fill:on"; done = true end if not done then for _, rule in ipairs(DE_ATTRS) do local rest = atom:match("^" .. rule.pref:gsub("[%-']", "%%%0") .. "%s+(.+)$") if rest and rule.f(trim(rest), out, ctx) then done = true; break end end end if not done then local pasde = atom:match("^un%s+pas%s+de%s+(.+)$") or atom:match("^pas%s+de%s+(.+)$") if pasde then pasde = trim(pasde) local m = pasde:match("^%$(.-)%$$") if m then out[#out + 1] = "step:{" .. m .. "}" else out[#out + 1] = "step:" .. num_fr(pasde) end done = true end local mesure = atom:match("^de%s+mesure%s+(.+)$") if mesure then out[#out + 1] = "label:{" .. num_fr(trim(mesure)) .. "}" done = true end local k, v = atom:match("^([%wéè_]+):(.+)$") if k == "unité" or k == "unite" then err("le réglage « unité: » n'existe plus : l'unité du repère se " .. "dit en prose — « avec une unité de 1 cm », ou « avec des " .. "unités respectives de 2 cm et 0,5 cm » (abscisses puis " .. "ordonnées).") end if k then local ek = V.KV_KEYS[k] or k v = v:gsub("^%$(.-)%$$", "{%1}") out[#out + 1] = ek .. ":" .. num_fr(v) done = true end end if not done and atom ~= "" then err("attribut non reconnu dans la phrase : « " .. atom .. " ». " .. "Attendu : « de côté N », « de rayon N », « en », " .. "« avec/sans ... », « avec N échantillons », « avec l'aire " .. "entre a et b », « avec le diagramme en escalier depuis U0 " .. "sur N termes », ou un réglage clé:valeur.") end end return true end local function parse_head(head) -- « Trace dans un repère ... » / « Représenter graphiquement dans un -- repère ... » : le « dans » de la forme académique se consomme avant -- l'article — il situe l'action, il ne nomme pas d'objet. head = head:match("^dans%s+(.+)$") or head local rest = head:match("^le%s+(.+)$") or head:match("^la%s+(.+)$") or head:match("^l'%s*(.+)$") or head:match("^un%s+(.+)$") or head:match("^une%s+(.+)$") or head rest = trim(rest) local seg1, seg2 = rest:match("^médiatrice%s+d.-%[%s*(%u)%s*,?%s*(%u)%s*%]") if seg1 then return "__prim perpbisector " .. seg1 .. " " .. seg2, "", "" end local b1, b2, b3 = rest:match("^bissectrice%s+de%s+l'angle%s+(%u)(%u)(%u)%s*$") if b1 then return "__prim anglebisector " .. b1 .. " " .. b2 .. " " .. b3, "", "" end local g1, g2, g3 = rest:match("^angle%s+(%u)(%u)(%u)%s*$") if g1 then return "__prim anglemark " .. g1 .. " " .. g2 .. " " .. g3, "", "" end local o1, e1, o2, e2 = rest:match("^angle%s+orienté%s+%((%u)(%u)%s*;%s*(%u)(%u)%)%s*$") if o1 then if o1 ~= o2 then err("l'angle orienté (" .. o1 .. e1 .. ";" .. o2 .. e2 .. ") demande deux demi-droites de même origine (" .. o1 .. " et " .. o2 .. " diffèrent).") end return "__prim anglemark " .. e1 .. " " .. o1 .. " " .. e2 .. " oriented:on", "", "" end local p1, q1, p2, q2 = rest:match("^angle%s+entre%s+(%u)(%u)%s+et%s+(%u)(%u)%s*$") if p1 then if p1 ~= p2 then err("l'angle entre " .. p1 .. q1 .. " et " .. p2 .. q2 .. " demande deux demi-droites de même origine (" .. p1 .. " et " .. p2 .. " diffèrent).") end return "__prim anglemark " .. q1 .. " " .. p1 .. " " .. q2, "", "" end local vn1, vn2 = rest:match("^angle%s+entre%s+le%s+vecteur%s+([%a_][%w_]*)%s+et%s+le%s+vecteur%s+([%a_][%w_]*)%s*$") if vn1 then return "__prim vecangle " .. vn1 .. " " .. vn2, "", "" end local vnm, vco = rest:match("^vecteur%s+([%a_][%w_]*)%s*(%b())%s*$") if vnm then return "__prim vector " .. vnm .. " " .. coords_fr(vco), "", "" end -- « le vecteur u depuis A / depuis (1 ; 3) / depuis la pointe de v » : -- redessine un vecteur déjà tracé, depuis un autre point de départ — -- c'est ainsi qu'on ferme le parallélogramme de la somme. local vrn, vrdep = rest:match("^vecteur%s+([%a_][%w_]*)%s+depuis%s+(.+)$") if vrn then local ptname = trim(vrdep):match("^(%u[%w]*)$") local coordexpr = trim(vrdep):match("^(%b())$") local tipname = trim(vrdep):match("^[Ll]a%s+pointe%s+de%s+([%a_][%w_]*)$") or trim(vrdep):match("^[Ll]e%s+bout%s+de%s+([%a_][%w_]*)$") local fromtok if ptname then fromtok = ptname elseif coordexpr then fromtok = coords_fr(coordexpr) elseif tipname then fromtok = "~" .. tipname else err("« depuis » attend un point déjà placé (depuis A), des " .. "coordonnées (depuis (1 ; 3)), ou la pointe d'un vecteur " .. "(depuis la pointe de v) — illisible : « " .. vrdep .. " ».") end return "__prim vector " .. vrn .. " from:" .. fromtok, "", "" end local vsn, va1, vop, va2, vtail = rest:match( "^vecteur%s+([%a_][%w_]*)%s*=%s*([%a_][%w_]*)%s*([%+%-])%s*([%a_][%w_]*)%s*(.-)$") if vsn then local dep = trim(vtail):match("^depuis%s+(.+)$") local fromtok if dep then local ptname = dep:match("^(%u[%w]*)$") local coordexpr = dep:match("^(%b())$") local tipname = dep:match("^[Ll]a%s+pointe%s+de%s+([%a_][%w_]*)$") or dep:match("^[Ll]e%s+bout%s+de%s+([%a_][%w_]*)$") if ptname then fromtok = ptname elseif coordexpr then fromtok = coords_fr(coordexpr) elseif tipname then fromtok = "~" .. tipname else err("« depuis » attend un point déjà placé (depuis A), des " .. "coordonnées (depuis (1 ; 3)), ou la pointe d'un vecteur " .. "(depuis la pointe de v) — illisible : « " .. dep .. " ».") end elseif trim(vtail) ~= "" then err("la somme ou la différence de deux vecteurs se dit « le vecteur w " .. "= u + v » ou « le vecteur w = u - v », suivi au besoin de " .. "« depuis ... » pour choisir son point de départ — illisible " .. "ici : « " .. vtail .. " ».") end local extra = fromtok and (" from:" .. fromtok) or "" return "__prim vector " .. vsn .. " = " .. va1 .. vop .. va2 .. extra, "", "" end local trig = rest:match("^cercle trigonométrique%s*(.*)$") if trig then return "trigcircle", "", trim(trig) end local repnat = rest:match("^repère%s+(.*)$") if repnat then local ortho if repnat:match("^orthonormé%s") or repnat == "orthonormé" then ortho = "orthonormé" elseif repnat:match("^orthogonal%s") or repnat == "orthogonal" then ortho = "orthogonal" end if ortho then repnat = trim(repnat:sub(#ortho + 1)) P._frame_adj = ortho end -- Nom académique du repère : (O, i, j) ou (O ; i ; j), toléré et -- consommé — il nomme le repère, il ne change pas son tracé. local nomrep, apresnom = repnat:match("^(%([^%)]*%))%s*(.*)$") if nomrep then repnat = trim(apresnom) end if repnat == "" then return "__frame", "", "" end local xa, xb, reste = repnat:match( "^où%s+l'abscisse%s+x?%s*appartient%s+à%s*%[%s*(.-)%s*;%s*(.-)%s*%]%s*(.*)$") if not xa then xa, xb, reste = repnat:match( "^où%s+l'abscisse%s+appartient%s+à%s*%[%s*(.-)%s*;%s*(.-)%s*%]%s*(.*)$") end if not xa then -- Tournure académique : « pour x appartient à [...] et y à [...] » xa, xb, reste = repnat:match( "^pour%s+x%s+appartient%s+à%s*%[%s*(.-)%s*;%s*(.-)%s*%]%s*(.*)$") end if xa then local ya, yb, apresy = reste:match( "^et%s+l'ordonnée%s+y?%s*appartient%s+à%s*%[%s*(.-)%s*;%s*(.-)%s*%]%s*(.*)$") if not ya then ya, yb, apresy = reste:match( "^et%s+l'ordonnée%s+y?%s*à%s*%[%s*(.-)%s*;%s*(.-)%s*%]%s*(.*)$") end if not ya then ya, yb, apresy = reste:match( "^et%s+y%s+appartient%s+à%s*%[%s*(.-)%s*;%s*(.-)%s*%]%s*(.*)$") end if not ya then ya, yb, apresy = reste:match( "^et%s+y%s+à%s*%[%s*(.-)%s*;%s*(.-)%s*%]%s*(.*)$") end if not ya then err("le repère se décrit : « un repère où l'abscisse appartient à " .. "[-5 ; 5] et l'ordonnée à [-5 ; 5] » ou « un repère pour x " .. "appartient à [-5 ; 5] et y à [-5 ; 5] » — l'ordonnée est " .. "illisible dans : « " .. reste .. " ».") end local head = "__framenat " .. num_fr(trim(xa)) .. " " .. num_fr(trim(xb)) .. " " .. num_fr(trim(ya)) .. " " .. num_fr(trim(yb)) return head, "", trim(apresy or "") end err("le repère se décrit en langage naturel : « un repère où " .. "l'abscisse appartient à [-5 ; 5] et l'ordonnée à [-5 ; 5] » " .. "ou « un repère (O, i, j) pour x appartient à [-5 ; 5] et y à " .. "[-5 ; 5] » (l'ancienne forme « de x -5 à 5 » n'existe plus).") end local rep = rest:match("^repère%s*$") if rep then return "__frame", "", "" end local ngrad = rest:match("^droite graduée%s*(.*)$") if ngrad then return "__numberline", "", trim(ngrad) end local fn = rest:match("^fonction%s+(.+)$") if fn then return "__function", "", trim(fn) end local solide = rest:match("^solide%s+(.+)$") if solide then local kind, srest = solide:match("^(%S+)%s*(.*)$") local ekind = SOLIDS[kind] if not ekind then err("solide inconnu : « " .. kind .. " » (attendu : cube, pavé, " .. "pyramide, cylindre, cône, sphère).") end return "__solid " .. ekind, srest:match("^(%u+)") or "", trim(srest:gsub("^%u+%s*", "")) end local dr = rest:match("^demi%-droite%s+(.+)$") if dr then return "__ray", "", trim(dr) end local seg = rest:match("^segment%s+de%s+droite%s+(.+)$") or rest:match("^segment%s+(.+)$") if seg then return "__segment", "", trim(seg) end local droite = rest:match("^droite%s+(.+)$") if droite then return "__line", "", trim(droite) end local kind, tail = rest:match("^(%S+)%s*(.*)$") local ekind = SPECIES[kind] if not ekind then return nil end local pts, quals = tail:match("^(%u[%u%d]*)%s*(.*)$") if not pts then pts, quals = "", tail end if kind == "polygone" then ekind = POLYGON_BY_N[#pts] or "polygon" end return ekind, pts, trim(quals or ""), (kind == "disque") end local function translate_quals(quals, out) if quals == "" then return end local at = quals:match("^rectangle%s+en%s+(%u)$") if at then out[#out + 1] = "right:" .. at; return end local Q = { ["équilatéral"] = "equilateral", ["isocèle"] = "isosceles", ["rectangle"] = "right" } for _, w in ipairs(split_et(quals)) do local q = Q[w] if q then out[#out + 1] = q .. ((q == "right") and "" or ":on") elseif w:match("^%b()$") then out.coords = coords_fr(w) else err("qualificatif inconnu : « " .. w .. " ».") end end end function P.translate_trace_phrase(phrase, ctx) phrase = trim(phrase) P._frame_adj = nil -- Factorisation plurielle du tracé (règle n°10) : « les fonctions f, g -- et h pour x dans [-2 ; 2] et y dans [-3 ; 3] » trace les trois courbes -- dans le même repère. Article pluriel (les, des) ou nombre en tête ; -- la liste de noms s'arrête au premier mot qui n'en est pas un. do local nb, suite = phrase:match("^(%d+)%s+fonctions%s+(.+)$") if not suite then suite = phrase:match("^[ld]es%s+fonctions%s+(.+)$") end if not suite then -- Forme académique : « les courbes des fonctions f et g » — -- c'est bien la courbe (représentation graphique) que l'on trace, -- pas la fonction elle-même. suite = phrase:match("^[ld]es%s+courbes%s+des%s+fonctions%s+(.+)$") if not suite then nb, suite = phrase:match("^(%d+)%s+courbes%s+des%s+fonctions%s+(.+)$") end end if suite then local RESERVES = { pour = true, sur = true, en = true, avec = true, sans = true, et = true, dans = true, de = true } local noms, pos = {}, 1 while true do local _, b, nom = suite:find("^%s*([%a][%w_]*)", pos) if not nom or RESERVES[nom] then break end noms[#noms + 1] = nom pos = b + 1 local _, b2 = suite:find("^%s*,%s*", pos) if b2 then pos = b2 + 1 else local _, b3 = suite:find("^%s+et%s+", pos) if b3 then pos = b3 + 1 else break end end end if #noms < 2 then err("le pluriel factorise : « les fonctions f, g et h pour x " .. "dans [-2 ; 2] et y dans [-3 ; 3] » — il faut au moins " .. "deux noms de fonctions, séparés par des virgules, « et » " .. "avant le dernier.") end if nb and tonumber(nb) ~= #noms then err("le compte n'y est pas : la phrase annonce " .. nb .. " fonctions mais en nomme " .. #noms .. ". Le nombre, " .. "comme l'article pluriel, factorise la déclaration : il " .. "doit correspondre au nombre de fonctions nommées.") end local out = {} local reste = trim(suite:sub(pos):gsub("^,%s*", "")) if reste ~= "" then for _, frag in ipairs(split_fragments(reste)) do frag = trim(frag) if frag ~= "" then translate_fragment(frag, out, ctx) end end end local words = { table.concat(noms, " ") } if out.x then words[#words + 1] = "x:" .. out.x end if out.y then words[#words + 1] = "y:" .. out.y end for _, w in ipairs(out) do words[#words + 1] = w end if ctx and ctx.block then local flines = {} for _, n in ipairs(noms) do flines[#flines + 1] = "function " .. n end return { kind = "draw_multi", lines = flines } end return { kind = "plot", line = table.concat(words, " ") } end end local sansart = phrase:match("^le%s+(.*)$") or phrase:match("^la%s+(.*)$") or phrase:match("^l'(.*)$") or phrase:match("^un%s+(.*)$") or phrase:match("^une%s+(.*)$") or phrase local pn, pc = sansart:match("^point%s+(%u[%w]*)%s*(%b())") if pn then return { kind = "draw", line = "point " .. pn .. " " .. coords_fr(pc) } end local pn2 = sansart:match("^point%s+(%u[%w]*)%s*$") if pn2 then return { kind = "draw", line = "point " .. pn2, needs = pn2 } end local br = sansart:match("^droite%s+%((%u+)%)") if br then return { kind = "draw", line = "line through " .. br:sub(1,1) .. " " .. br:sub(2,2) } end -- Une fonction déjà posée par , tracée dans le repère qui -- l'entoure (au lieu de son propre repère, comme fait hors bloc). Seule forme utile ici : le nom seul, -- puisque la fenêtre et les unités viennent déjà du bloc englobant. local ffn = sansart:match("^fonction%s+([%a_][%w_]*)%s*$") or sansart:match("^courbe%s+de%s+la%s+fonction%s+([%a_][%w_]*)%s*$") if ffn then return { kind = "draw", line = "function " .. ffn } end local deq = sansart:match("^droite%s+d['’]équation%s+y%s*=%s*(.+)$") if deq then return { kind = "draw", line = "curve " .. num_fr(trim(deq)) } end local frags = split_fragments(phrase) local ekind, pts, quals, filled = parse_head(frags[1]) if not ekind then err("figure non reconnue : « " .. frags[1] .. " ». Espèces connues : " .. "triangle, carré, rectangle, losange, cercle, disque, polygone, " .. "repère, fonction, point, droite, segment, solide, " .. "cercle trigonométrique, droite graduée.") end local out = {} if not ekind:match("^__") and ekind ~= "trigcircle" then translate_quals(quals, out) end if filled then out[#out + 1] = "fill:on" end for i = 2, #frags do translate_fragment(frags[i], out, ctx) end if ekind == "__function" then local fname = pts ~= "" and pts or quals:match("^(%S+)") or "" if fname == "" then fname = frags[1]:match("fonction%s+(%S+)") end local reste = quals:gsub("^%S+%s*", "") if reste ~= "" then translate_fragment(reste, out, ctx) end local words = { fname } if out.x then words[#words + 1] = "x:" .. out.x end if out.y then words[#words + 1] = "y:" .. out.y end for _, w in ipairs(out) do words[#words + 1] = w end return { kind = "plot", line = table.concat(words, " ") } end local fa, fb, fc, fd = ekind:match("^__framenat%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)$") if fa or ekind == "__frame" then -- La forme académique porte ses unités à la suite, sans virgule : -- « ... et y à [-9 ; 9] avec des unités graphiques de ... » — ce -- reliquat arrive dans quals et se traduit comme un fragment. if quals and trim(quals) ~= "" then translate_fragment(trim(quals), out, ctx) end if P._frame_adj == "orthonormé" and out.unitx and out.unity and out.unitx ~= out.unity then err("un repère orthonormé porte la même unité sur les deux axes : " .. "« avec une unité de N cm ». Pour des unités distinctes, " .. "dites « un repère orthogonal, avec des unités respectives " .. "de ... et ... ».") end end if fa then return { kind = "frame", x = "{" .. fa .. ", " .. fb .. "}", y = "{" .. fc .. ", " .. fd .. "}", opts = out } end if ekind == "__frame" then return { kind = "frame", x = out.x, y = out.y, opts = out } end if ekind == "__numberline" then if quals ~= "" then translate_fragment(quals, out, ctx) end local words = {} if out.x then words[#words + 1] = "x:" .. out.x end for _, w in ipairs(out) do w = w:gsub("^color:([%w]+)$", "color:{%1}") words[#words + 1] = w end return { kind = "numberline", line = table.concat(words, " ") } end if ekind == "__segment" or ekind == "__ray" or ekind == "__line" then local brut = trim((pts ~= "" and (pts .. " ") or "") .. (quals or "")) for _, w in ipairs(out) do brut = brut .. " " .. w end brut = trim(brut) local sep_o, A, B, sep_c = brut:match("^([%[%(])(%u)(%u)([%]%)])") if not A then err("la " .. (ekind == "__ray" and "demi-droite" or ekind == "__line" and "droite" or "segment") .. " se nomme par ses deux points, par exemple « [AB] » " .. "(segment), « (AB] » (demi-droite) ou « (AB) » (droite).") end if ekind == "__ray" then -- L'ancre (extrémité fixe) est celle du côté fermé — « [AB) » part -- de A, « (AB] » part de B — on la remet toujours en tête, pour -- que le moteur de tracé n'ait qu'un seul ordre à connaître. if sep_o == "(" then A, B = B, A end end local reste = brut:sub(5) local la, lval, lunit = reste:match( "tel%a*%s+que%s+(%u%u)%s*=%s*([%d,%.]+)%s*(c?m?m?)") local mode = (ekind == "__ray") and "ray" or (ekind == "__line") and "line" or "segment" local mots = { mode, A, B } if lval then if lunit == "" then err("la longueur d'une construction s'écrit avec son unité : " .. "« tel que " .. la .. " = " .. lval .. " cm » ou « " .. lval .. " mm » — l'unité (cm ou mm) est obligatoire.") end mots[#mots + 1] = "length:" .. num_fr(lval) mots[#mots + 1] = "measures:" .. lunit end for _, w in ipairs(out) do if w ~= A .. B then mots[#mots + 1] = w end end return { kind = "draw", line = table.concat(mots, " ") } end local head = ekind if ekind:match("^__solid%s") then head = ekind:gsub("^__", "") end if ekind:match("^__prim%s") and not ekind:match("^__framenat") then if not (ctx and (ctx.frame or ctx.block)) then err("la médiatrice, la bissectrice et la marque d'angle se tracent " .. "dans une figure : placez cette phrase à l'intérieur d'un bloc " .. "{...} où les points concernés sont définis (sommets " .. "d'une figure tracée, ou points posés dans un repère).") end head = ekind:gsub("^__prim%s+", "") end local words = { head } if pts ~= "" then words[#words + 1] = pts end if out.coords then words[#words + 1] = out.coords end for _, w in ipairs(out) do words[#words + 1] = w end if out.measures then words[#words + 1] = "measures:" .. out.measures end return { kind = "draw", line = table.concat(words, " ") } end function P.translate_soit_decl(decl) decl = trim(decl) if decl:match("^une?%s+point.-%s+et%s+une?%s+point") or decl:match("^un%s+point%s+.-%s+et%s+un%s+point") then err("la règle de factorisation de texecole regroupe les points sous " .. "un seul article pluriel : écrivez « les points A(2;3) et " .. "B(-1;4) » plutôt que « un point... et un point... ».") end if decl:match("^une%s+fonction%s+.-%s+et%s+une%s+fonction") then err("la règle de factorisation de texecole regroupe les fonctions " .. "sous un seul article pluriel : écrivez « les fonctions " .. "f(x) = ... et g(x) = ... » plutôt que « une fonction... et " .. "une fonction... ».") end local cn, cd, cc, cdef = decl:match( "^un%s+champ%s+de%s+vecteurs%s+(%u[%w_]*)%s+de%s+(%S+)%s+dans%s+(%S+)%s+défini%s+par%s*:?%s*(.+)$") if cn then return { display = "un champ de vecteurs $" .. cn .. " : " .. cd .. " -> " .. cc .. "$ défini par $" .. num_fr(cdef) .. "$" } end local fdef = decl:match("^une%s+fonction%s+(.+)$") if fdef then return { display = "une fonction $" .. fdef .. "$", fn = num_fr(fdef) } end local fliste = decl:match("^les%s+fonctions%s+(.+)$") or decl:match("^des%s+fonctions%s+(.+)$") if fliste then local morceaux, buf, depth = {}, {}, 0 local src = fliste:gsub("%s+et%s+", ", ") for i = 1, #src do local ch = src:sub(i, i) if ch == "(" then depth = depth + 1 end if ch == ")" then depth = depth - 1 end local decimale = ch == "," and src:sub(i - 1, i - 1):match("%d") and src:sub(i + 1, i + 1):match("%d") if ch == "," and depth == 0 and not decimale then morceaux[#morceaux + 1] = table.concat(buf); buf = {} else buf[#buf + 1] = ch end end if #buf > 0 then morceaux[#morceaux + 1] = table.concat(buf) end local fns, displays = {}, {} for _, morceau in ipairs(morceaux) do morceau = trim(morceau) if morceau ~= "" then if not morceau:match("^[%a_][%w_]*%s*%([%a_][%w_]*%)%s*=%s*%S") then err("fonction illisible dans la liste : « " .. morceau .. " » — attendu NOM(var) = expression, par exemple f(x) = x^2.") end fns[#fns + 1] = num_fr(morceau) displays[#displays + 1] = "$" .. morceau .. "$" end end if #fns == 0 then err("aucune fonction dans « " .. decl .. " ».") end local aff = table.concat(displays, ", ") if #displays > 1 then aff = table.concat(displays, ", ", 1, #displays - 1) .. " et " .. displays[#displays] end return { display = "les fonctions " .. aff, fns = fns } end local nm, co = decl:match("^un%s+point%s+(%u[%w]*)%s*(%b())$") if nm then return { display = decl, points = { { nom = nm, co = coords_fr(co) } } } end local nb, liste = decl:match("^(%d+)%s+points%s+(.+)$") if not liste then liste = decl:match("^[ld]es%s+points%s+(.+)$") end if liste then local pts = {} -- Forme distributive : « les points A, B et C de coordonnées -- respectives (1 ; 2), (-1 ; 2) et (-1 ; -2) » — les noms d'abord, -- les coordonnées ensuite, appariés dans l'ordre. local noms_txt, coords_txt = liste:match( "^(.-)%s+de%s+coordonnées%s+respectives%s+(.+)$") if noms_txt then local noms = {} for nom in noms_txt:gsub("%s+et%s+", ", "):gmatch("[^,]+") do nom = trim(nom) if nom ~= "" then if not nom:match("^%u[%w]*$") then err("nom de point illisible dans la liste : « " .. nom .. " » — attendu une majuscule, par exemple A, B ou C1.") end noms[#noms + 1] = nom end end local coords = {} local reste = coords_txt:gsub("%s+et%s+", ", ") for co in reste:gmatch("%b()") do coords[#coords + 1] = coords_fr(co) end local hors = reste:gsub("%b()", ""):gsub("[,%s]", "") if hors ~= "" then err("coordonnées illisibles dans « " .. coords_txt .. " » — " .. "attendu des couples (x ; y) séparés par des virgules, " .. "« et » avant le dernier.") end if #noms == 0 or #coords == 0 then err("la forme distributive demande des noms puis des couples : " .. "« les points A, B et C de coordonnées respectives " .. "(1 ; 2), (-1 ; 2) et (-1 ; -2) ».") end if #noms ~= #coords then err("distribution respective boiteuse : " .. #noms .. " nom" .. (#noms > 1 and "s" or "") .. " de points pour " .. #coords .. " couple" .. (#coords > 1 and "s" or "") .. " de " .. "coordonnées — « respectives » exige autant de couples " .. "que de noms, dans le même ordre.") end for i = 1, #noms do pts[#pts + 1] = { nom = noms[i], co = coords[i] } end else local morceaux, buf, depth = {}, {}, 0 local src = liste:gsub("%s+et%s+", ", ") for i = 1, #src do local ch = src:sub(i, i) if ch == "(" then depth = depth + 1 end if ch == ")" then depth = depth - 1 end if ch == "," and depth == 0 then morceaux[#morceaux + 1] = table.concat(buf); buf = {} else buf[#buf + 1] = ch end end if #buf > 0 then morceaux[#morceaux + 1] = table.concat(buf) end for _, morceau in ipairs(morceaux) do morceau = trim(morceau) if morceau ~= "" then local n2, c2 = morceau:match("^(%u[%w]*)%s*(%b())$") if not n2 then err("point illisible dans la liste : « " .. morceau .. " » — attendu NOM(x;y), par exemple A(-1,5;3).") end pts[#pts + 1] = { nom = n2, co = coords_fr(c2) } end end end if #pts == 0 then err("aucun point dans « " .. decl .. " ».") end if nb and tonumber(nb) ~= #pts then err("le compte n'y est pas : la phrase annonce " .. nb .. " points " .. "mais en décrit " .. #pts .. ". Le nombre, comme l'article " .. "pluriel, factorise la déclaration : il doit correspondre au " .. "nombre de points décrits.") end return { display = decl, points = pts } end err("déclaration non reconnue après : « " .. decl .. " ». " .. "Attendu : « un point A(x;y) », « les points A(x;y), B(x;y)... », " .. "« 3 points A(x;y), B(x;y) et C(x;y) », « les points A, B et C " .. "de coordonnées respectives (x;y), (x;y) et (x;y) », " .. "« une fonction f(x) = ... » ou « les fonctions f(x) = ..., " .. "g(x) = ... et h(x) = ... » (article, règle n°10).") end return P