Module:Flowchart

De Wikimedica
Aller à la navigation Aller à la recherche


1 Utilisation[w]

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.

1.1 Nœuds[w]

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)

1.2 Liens[w]

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.

1.3 Groupes[w]

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).

2 Exemple[w]

{{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
}}

3 Paramètres[w]

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

Paramètres du modèle

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

ParamètreDescriptionTypeStatut
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
Déboggage$debug

Active le mode déboggage

Par défaut
0
Booléenfacultatif

4 Notes techniques[w]

  • 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 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 = '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, links = p.read_flowchart(args)
	return p.render_mermaid(frame, params, nodes, groups, links)
end

return p