Module:Flowchart

De Wikimedica
Révision datée du 29 juillet 2020 à 12:11 par Mattéo Delabre (discussion | contributions) (Liens fléchés par défaut, renomme ".interpolation" en ".shape" pour cohérence)


Utilisation

Page principale: Aide:Diagramme

Permet d'afficher un digramme de type "Flowchart" (algorithme). L'utilisation de ce modèle est différente car il ne possède pas de nombre de paramètres définis et les paramètres sont nommés selon la fonction qu'ils occupent dans le flowchart. Seuls les paramètres débutant par $ sont fixes.

Il est conseillé de passer le wikicode pour faire l'édition de ce modèle. L'éditeur visuel n'étant pas adapté à ce cas d'utilisation.

Nœuds

Sous la forme id nœud=Étiquette à afficher pour le nœud, quasiment tous les caractères sont autorisés dans l’identifiant du nœud, sauf ceux utilisés pour déclarer un lien, un paramètre général ou un groupe. On peut en plus définir des propriétés pour les nœuds, telles que :

  • id nœud.shape=... spécifie la forme du nœud, parmi rounded (par défaut), rectangle, circle, flag, diamond.
  • id nœud.group=... définit le groupe de nœuds auquel appartient ce nœud, par défaut aucun.
  • id nœud.style.fill=... couleur de remplissage du nœud, par défaut c’est celle du thème Mermaid actif.
  • id nœud.style.stroke=... idem pour la bordure du nœud ( exemple: red,stroke-width:4px,stroke-dasharray: 5 5)

Liens

Ajout d’un lien entre deux nœuds. Sous la forme id nœud d’origine -> id nœud d’arrivée, ou id nœud d’origine -> id nœud d’arrivée=Étiquette du lien pour ajouter une étiquette. Comme pour les définitions de nœuds, on peut définir des propriétés pour le lien, telles que :

  • id nœud d’origine -> id nœud d’arrivée.shape=... spécifie la forme du lien, parmi curve (par défaut), linear, step, step before, step after.
  • id nœud d’origine -> id nœud d’arrivée.ending=... peut valoir arrow pour ajouter une flèche au bout du lien (par défaut), ou plain pour ne rien y mettre.

Groupes

Définition d’un groupe de nœuds. Les nœuds sont ajoutés à un groupe en affectant leur propriété .group=id du groupe. On peut en plus spécifier l’étiquette du groupe avec la syntaxe group id du groupe=Étiquette du groupe (préfixé de group).

Exemple

{{Flowchart
| $orientation = to right
| Start = Boîte de début
| Start -> A
| A = Lien vers une [[AAA|maladie]]
| A -> End
| End.group = groupe
| group groupe = Un groupe
| Start -> B
| B = Texte en '''gras'''<br>Avec une autre ligne.<br> Et une autre.
| B -> B2
| B2 = Une condition?
| B2.shape = diamond
| B2.style.fill = #ff6666
| B2 -> B3 = Oui
| B3.group = groupe
| B2 -> End = Non
| Start -> C
| C -> Fin 2
| Fin 2.level = 2
| Fin 2.shape = rectangle
| Fin 2.style.fill = #ff9900
}}

Paramètres

Génère un diagramme de type Flowchart (algorithme)

[Modifier les données du modèle]

Paramètres du modèle

La mise en forme multiligne est préférée pour ce modèle.

ParamètreDescriptionTypeÉtat
titre$titre

Titre du graphique

Contenufacultatif
Orientation$orientation

Orientation du flowchart, peut être to bottom, to right, to top, to left.

Par défaut
to bottom
Chaînefacultatif
Largeur$largeur

Force une largeur en pixels. Utile pour agrandir les flowcharts imposants.

Exemple
1200
Nombrefacultatif
Déboggage$debug

Active le mode déboggage

Par défaut
0
Booléenfacultatif

Notes techniques

  • En raison d'un conflit avec l'extension Flow, l'affichage des Flowcharts dans les discussions ne fonctionne pas car la librairie mermaid n'est pas chargée. Un patch a été ajouté dans MediaWiki:Common.js pour pallier à ce problème. Un bogue a été soumis auprès des développeurs.
  • De base, Lua ne parse pas de Wikicode à l'interne, une fonction de parsing a donc été rajoutée à LocalSettings.js

local getArgs = require("Module:Arguments").getArgs
local p = {}

--- Récupère la liste des clés d’un tableau associatif
local function get_table_keys(tab)
	local keys = {}
	
	for key, _ in pairs(tab) 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

--[======[ Représentation intermédiaire du flowchart ]======]--

local link_separator = "->"
local property_separator = "."
local group_prefix = "group "

--- Lit la définition d’un flowchart
function p.read_flowchart(args)
	local nodes = {}
	local groups = {}
	local edges = {}
	
	for key, value in pairs(args) do
		if type(key) == "number" then
			key = value
			value = true
		end
		
		local arrow_index = string.find(key, link_separator, 1, true)
		local is_group = string.find(key, group_prefix, 1, true)
		
		if is_group then
			-- Concerne un groupe de nœuds
			key = string.sub(key, #group_prefix + 1)
			
			if value ~= true then
				if groups[key] then
					groups[key].name = value
				else
					groups[key] = {name = value}
				end
			else
				groups[key] = {}
			end
		elseif arrow_index then
			-- Concerne un lien
			local from = mw.text.trim(string.sub(key, 1, arrow_index - 1))
			local to = mw.text.trim(string.sub(key, arrow_index + 2))
			local dot_index = string.find(to, property_separator, 1, true)
			
			if not nodes[from] then
				nodes[from] = {}
			end
			
			if not dot_index then
				-- Définition d’un lien (de la forme "départ->arrivée=étiquette du lien")
				local id = from .. "\0" .. to
			
				if not nodes[to] then
					nodes[to] = {}
				end
				
				if value ~= true then
					if edges[id] then
						edges[id].name = value
					else
						edges[id] = {name = value}
					end
				elseif not edges[id] then
					edges[id] = {}
				end
			else
				-- Propriété d’un lien (de la forme "départ->arrivée.propriété=valeur")
				local property = mw.text.trim(string.sub(to, dot_index + 1))
				to = mw.text.trim(string.sub(to, 1, dot_index - 1))
				local id = from .. "\0" .. to
				
				if not nodes[to] then
					nodes[to] = {}
				end
				
				if edges[id] then
					edges[id][property] = value
				else
					edges[id] = {[property] = value}
				end
			end
		else
			-- Concerne un nœud
			local dot_index = string.find(key, property_separator, 1, true)
			
			if not dot_index then
				-- Définition d’un nœud (de la forme "nœud=étiquette du nœud")
				if value ~= true then
					if nodes[key] then
						nodes[key].name = value
					else
						nodes[key] = {name = value}
					end
				elseif not nodes[key] then
					nodes[key] = {}
				end
			else
				-- Propriété d’un nœud (clé de la forme "nœud.propriété=valeur")
				local property = mw.text.trim(string.sub(key, dot_index + 1))
				key = mw.text.trim(string.sub(key, 1, dot_index - 1))
				mw.log(key, property, value)
				
				if nodes[key] then
					nodes[key][property] = value
				else
					nodes[key] = {[property] = value}
				end
				
				if property == "group" then
					if not groups[value] then
						groups[value] = {}
					end
				end
			end
		end
	end
	
	return nodes, groups, edges
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(params, nodes, groups, edges)
	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 "rectangle"
		
		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
		
		table.insert(lines, string.format(nodes_shapes[shape], id, escape_quotes(name)))
	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
	
	for key, properties in pairs(edges) do
		local ends = mw.text.split(key, "\0", true)
		local from = generate_id(ends[1])
		local to = generate_id(ends[2])
		
		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
		
		if properties.name then
			table.insert(lines, string.format('%s -- "%s" %s %s', from, escape_quotes(properties.name), endings[ending], to))
		else
			table.insert(lines, string.format("%s %s %s", from, endings[ending], to))
		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, edges)
	local mermaid = p.flowchart_to_mermaid(params, nodes, groups, edges)
	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_edges = mw.text.nowiki(mw.dumpObject(edges))
		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>edges</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_edges, dump_mermaid, rendered_mermaid)
	else
		return rendered_mermaid
	end
end

--[======[ Interface ]======]--

function p.render(frame)
	-- Normalise les arguments
	local args = getArgs(frame, {
		wrappers = 'Modèle: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, edges = p.read_flowchart(args)
	return p.render_mermaid(frame, params, nodes, groups, edges)
end

return p