Module:Flowchart
From WikiMSK
local p = {}
--[======[ Fonctions utilitaires ]======]--
local getArgs = require("Module:Arguments").getArgs
--- Récupère la liste des clés d’un tableau associatif
local function get_table_keys(t)
local keys = {}
for key, _ in pairs(t) do
table.insert(keys, '"' .. key .. '"')
end
return table.concat(keys, ", ")
end
--- Convertit un identifiant d’objet en identifiant utilisé en syntaxe Mermaid
local function generate_id(id)
-- On utilise une version hachée de l’identifiant de chaque objet (avec un
-- algorithme de hachage faible, acceptable ici car nous n’en faisons pas
-- une utilisation cryptographique) afin de permettre l’utilisation de
-- caractères arbitraires dans le Wikicode sans poser de problèmes au
-- parseur Mermaid
return mw.hash.hashValue("md5", id)
end
--- Échappe les guillemets droits utilisés dans une étiquette
local function escape_quotes(label)
return string.gsub(label, '"', "#quot;")
end
--- Découpe une chaîne autour de la première occurrence d’un délimiteur
--- et ignore les caractères blancs autour des deux segments résultant
-- Liste des caractères Unicode blancs, tirée de
-- <https://en.wikipedia.org/wiki/Unicode_character_property#Whitespace>
local whitespace = mw.ustring.char(
0x9, 0xA, 0xB, 0xC, 0xD, 0x20, 0x85, 0xA0, 0x1680,
0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006,
0x2007, 0x2008, 0x2009, 0x200A, 0x2028, 0x2029, 0x202F,
0x205F, 0x3000
)
local function split_delimiter(s, delim)
local d_start, d_end = string.find(s, delim, 1, true)
if d_start and d_end then
local left = mw.text.trim(string.sub(s, 1, d_start - 1), whitespace)
local right = mw.text.trim(string.sub(s, d_end + 1), whitespace)
return left, right
else
return s
end
end
--- Affecte une valeur dans une clé d’une table, en utilisant un chemin en
--- notation pointée
local function set_key_path(t, key, default_key, value)
local left, right = split_delimiter(key, ".")
if not right and default_key then
right = default_key
end
if not right then
t[left] = value
else
if not t[left] then
t[left] = {}
end
set_key_path(t[left], right, nil, value)
end
end
--[======[ Représentation intermédiaire du flowchart ]======]--
local link_separator = "->"
local group_prefix = "group "
--- Lit la définition d’un flowchart
function p.read_flowchart(args)
local nodes = {}
local groups = {}
local links = {}
for key, value in pairs(args) do
if type(key) == "number" then
key = value
value = nil
end
local is_group = string.find(key, group_prefix, 1, true) == 1
local is_link = string.find(key, link_separator, 1, true)
if is_group then
-- Concerne un groupe de nœuds
local group_key = string.sub(key, #group_prefix + 1)
set_key_path(groups, group_key, "name", value)
elseif is_link then
-- Concerne un lien
local base, rest = split_delimiter(key, ".")
local from, to = split_delimiter(base, link_separator)
local link_key = nil
if not rest then
link_key = string.format("%s->%s", from, to)
else
link_key = string.format("%s->%s.%s", from, to, rest)
end
if not nodes[from] then
nodes[from] = {}
end
if not nodes[to] then
nodes[to] = {}
end
set_key_path(links, link_key, "name", value)
else
-- Concerne un nœud
set_key_path(nodes, key, "name", value)
end
end
return nodes, groups, links
end
--[======[ Génération de code Mermaid ]======]--
local orientations = {
["to bottom"] = "TB",
["to top"] = "BT",
["to right"] = "LR",
["to left"] = "RL",
}
local nodes_shapes = {
rectangle = '%s["%s"]',
rounded = '%s("%s")',
circle = '%s(("%s"))',
flag = '%s>"%s"]',
diamond = '%s{"%s"}',
}
local endings = {
plain = "---",
arrow = "-->",
}
local links_shapes = {
curve = "basis",
linear = "linear",
["step before"] = "stepBefore",
["step after"] = "stepAfter",
step = "step",
}
--- Génère le code Mermaid pour un flowchart
function p.flowchart_to_mermaid(frame, params, nodes, groups, links)
local lines = {}
local orientation = params.orientation or "to bottom"
if not orientations[orientation] then
error(string.format(
'Orientation "%s" inconnue pour le flowchart. Les orientations possibles sont %s.',
orientation, get_table_keys(orientations)
), 0)
end
table.insert(lines, string.format("graph %s", orientations[orientation]))
-- Liste des nœuds membres de chaque groupe, construite en même temps
-- qu’on itère sur les nœuds pour les insérer dans la sortie
local groups_members = {}
for key, _ in pairs(groups) do
groups_members[key] = {}
end
-- Génère les nœuds
for key, properties in pairs(nodes) do
local id = generate_id(key)
local name = properties.name or key
local shape = properties.shape or "rounded"
if properties.group then
table.insert(groups_members[properties.group], id)
end
if not nodes_shapes[shape] then
error(string.format(
'Forme "%s" inconnue pour le nœud "%s". Les formes possibles sont %s.',
shape, name, get_table_keys(nodes_shapes)
), 0)
end
-- Forme et étiquette du nœud
table.insert(lines, string.format(nodes_shapes[shape], id, escape_quotes(name)))
-- Style
if properties.style then
local rules = {}
for property, value in pairs(properties.style) do
table.insert(rules, string.format("%s:%s", property, value))
end
table.insert(lines, string.format("style %s %s", id, table.concat(rules, ",")))
end
end
-- Génère les groupes
for key, properties in pairs(groups) do
local name = properties.name or key
table.insert(lines, string.format('subgraph %s', name))
for _, node in pairs(groups_members[key]) do
table.insert(lines, node)
end
table.insert(lines, "end")
end
-- Génère les liens
local link_number = 0
-- Classe CSS pour les noeuds invisibles
table.insert(lines, "classDef SkipLevel width:0px;")
for key, properties in pairs(links) do
local from, to = split_delimiter(key, link_separator)
local from_id = generate_id(from)
local to_id = generate_id(to)
local ending = properties.ending or "arrow"
if not endings[ending] then
error(string.format(
'Terminaison "%s" inconnue pour le lien "%s -> %s". Les terminaisons possibles sont %s.',
shape, from, to, get_table_keys(endings)
), 0)
end
-- Définit les noeuds permettant de sauter de niveau.
-- Voir https://github.com/mermaid-js/mermaid/issues/637 pour la technique
-- qui consiste en rajouter des noeuds invisibles.
local level = nodes[to].level
if level ~= '-' and level ~= nil then
level = tonumber(level)
if level == nil or level < 1 then
error(string.format('Le niveau du noeud %s doit être un nombre entier > 0.', to), 0)
end
local prev_id = from_id
local class = "class " .. from_id .. '-0'
for i = 1, level - 1 do
-- Ajoute un noeud invisible
local next_id = from_id .. '-' .. i
class = class .. ',' .. next_id
table.insert(lines, next_id .. '( )')
-- Ajoute une connexion entre les noeuds invisibles
table.insert(lines, prev_id .. " --- " .. next_id)
prev_id = next_id
end
table.insert(lines, class .. " SkipLevel")
-- La dernière connexion sera créée normalement
from_id = prev_id
end
if properties.name then
table.insert(lines, string.format('%s -- "%s" %s %s', from_id, escape_quotes(properties.name), endings[ending], to_id))
else
table.insert(lines, string.format("%s %s %s", from_id, endings[ending], to_id))
end
local shape = properties.shape or "curve"
if not links_shapes[shape] then
error(string.format(
'Forme "%s" inconnue pour le lien "%s -> %s". Les formes possibles sont %s.',
shape, from, to, get_table_keys(links_shapes)
), 0)
end
if shape ~= "linear" then
table.insert(lines, string.format("linkStyle %d interpolate %s", link_number, links_shapes[shape]))
end
link_number = link_number + 1
end
return table.concat(lines, "\n")
end
local themes = {
default = true,
neutral = true,
forest = true,
dark = true,
}
--- Appelle l’extension Mermaid
function p.render_mermaid(frame, params, nodes, groups, links)
local mermaid = p.flowchart_to_mermaid(frame, params, nodes, groups, links)
local theme = params.theme or "neutral"
if not themes[theme] then
error(string.format(
'Thème "%s" inconnu pour le flowchart. Les thèmes possibles sont %s.',
theme, get_table_keys(themes)
), 0)
end
local rendered_mermaid = frame:callParserFunction(
"#mermaid", mermaid,
"config.theme = " .. theme
)
if params.debug then
dump_params = mw.text.nowiki(mw.dumpObject(params))
dump_nodes = mw.text.nowiki(mw.dumpObject(nodes))
dump_groups = mw.text.nowiki(mw.dumpObject(groups))
dump_links = mw.text.nowiki(mw.dumpObject(links))
dump_mermaid = mw.text.nowiki(mermaid)
return string.format([[
<h4>Représentation interne</h4>
<table>
<tr>
<th>params</th>
<th>nodes</th>
<th>groups</th>
<th>links</th>
</tr>
<tr>
<td><pre>%s</pre></td>
<td><pre>%s</pre></td>
<td><pre>%s</pre></td>
<td><pre>%s</pre></td>
</tr>
</table>
<h4>Code Mermaid</h4>
<pre>%s</pre>
<h4>Résultat</h4>
%s
]], dump_params, dump_nodes, dump_groups, dump_links, dump_mermaid, rendered_mermaid)
else
return rendered_mermaid
end
end
--[======[ Interface ]======]--
function p.render(frame)
-- Normalise les arguments
local args = getArgs(frame, {
wrappers = 'Module:Flowchart'
})
-- Lit et consomme les paramètres généraux du flowchart préfixés par "$"
local params = {}
for args_key, value in pairs(args) do
local key = args_key
if type(args_key) == "number" then
key = value
value = "true"
end
if string.sub(key, 1, 1) == "$" then
params[string.sub(key, 2)] = value
args[args_key] = nil
end
end
local nodes, groups, links = p.read_flowchart(args)
return p.render_mermaid(frame, params, nodes, groups, links)
end
return p