/*
 * $Id$
 *
 * SARL is an general-purpose agent programming language.
 * More details on http://www.sarl.io
 *
 * Copyright (C) 2014-2026 SARL.io, the original authors and main authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sarl.docs.generator.markdown;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.inject.Inject;

import org.arakhne.afc.vmutil.FileSystem;
import org.arakhne.afc.vmutil.URISchemeType;
import org.eclipse.xtext.util.Strings;
import org.eclipse.xtext.xbase.compiler.output.ITreeAppendable;
import org.eclipse.xtext.xbase.lib.Functions.Function2;
import org.eclipse.xtext.xbase.lib.IntegerRange;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;

import com.google.common.collect.Iterables;
import com.vladsch.flexmark.ast.Heading;
import com.vladsch.flexmark.ast.Image;
import com.vladsch.flexmark.ast.Link;
import com.vladsch.flexmark.ast.Paragraph;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.Node;
import com.vladsch.flexmark.util.ast.NodeVisitor;
import com.vladsch.flexmark.util.ast.VisitHandler;
import com.vladsch.flexmark.util.data.MutableDataSet;
import com.vladsch.flexmark.util.sequence.BasedSequence;

import io.sarl.docs.generator.bugfixes.FileSystemAddons;
import io.sarl.docs.generator.parser.AbstractMarkerLanguageParser;
import io.sarl.docs.generator.parser.DynamicValidationComponent;
import io.sarl.docs.generator.parser.DynamicValidationContext;
import io.sarl.docs.generator.parser.SarlDocumentationParser;
import io.sarl.docs.generator.parser.SectionNumber;
import io.sarl.docs.validator.ReflectExtensions;

/** Markdown parser.
 *
 * @author $Author: sgalland$
 * @version $FullVersion$
 * @mavengroupid $GroupId$
 * @mavenartifactid $ArtifactId$
 * @since 0.6
 */
public class MarkdownParser extends AbstractMarkerLanguageParser {

	/** List of the filename extensions that corresponds to Markdown files.
	 */
	public static final String[] MARKDOWN_FILE_EXTENSIONS = new String[] {
		".md", ".markdown", ".mdown", //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
		".mkdn", ".mkd", ".mdwn", //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
		".mdtxt", ".mdtext", //$NON-NLS-1$//$NON-NLS-2$
	};

	/** Default level at which the titles may appear in the outline.
	 */
	public static final int DEFAULT_OUTLINE_TOP_LEVEL = 2;

	/** Indicates if the sections should be numbered by default.
	 */
	public static final boolean DEFAULT_SECTION_NUMBERING = true;

	/** Indicates if a hyperlinks to the operation should be created for each operation name,
	 * that is generated.
	 */
	public static final boolean DEFAULT_ADD_LINK_TO_OPERATION_NAME = true;

	/** Indicates the default name of the style for the outline.
	 */
	public static final String DEFAULT_OUTLINE_STYLE_ID = "page_outline"; //$NON-NLS-1$

	/** The default format, compatible with {@link MessageFormat} for the section titles.
	 */
	public static final String DEFAULT_SECTION_TITLE_FORMAT = "{0} {1}. {2}"; //$NON-NLS-1$

	/** The default format, compatible with {@link MessageFormat} for the outline entry without auto-numbering.
	 */
	public static final String DEFAULT_OUTLINE_ENTRY_WO_AUTONUMBERING = "{0} [{1}](#{2})"; //$NON-NLS-1$

	/** The default format, compatible with {@link MessageFormat} for the outline entry with auto-numbering.
	 */
	public static final String DEFAULT_OUTLINE_ENTRY_W_AUTONUMBERING = "{0} [{1}. {2}](#{3})"; //$NON-NLS-1$

	/** Name of the property that contains the pattern for information notes.
	 * @since 0.12
	 */
	public static final String INFO_NOTE_PATTERN_PROPERTY = "io.sarl.maven.docs.generator.infonote"; //$NON-NLS-1$

	/** Name of the property that contains the pattern for warning notes.
	 * @since 0.12
	 */
	public static final String WARNING_NOTE_PATTERN_PROPERTY = "io.sarl.maven.docs.generator.warningnote"; //$NON-NLS-1$

	/** Name of the property that contains the pattern for danger notes.
	 * @since 0.12
	 */
	public static final String DANGER_NOTE_PATTERN_PROPERTY = "io.sarl.maven.docs.generator.dangernote"; //$NON-NLS-1$

	private static final String MARKDOWN_INFORMATION_NOTE_PATTERN1 = "^\\>[ \t\n\r]*\\*\\*\\_(.*?):?\\_\\*\\*(.*)$"; //$NON-NLS-1$

	private static final String MARKDOWN_INFORMATION_NOTE_PATTERN2 = "^\\>(.*)$"; //$NON-NLS-1$

	private static final String SECTION_PATTERN_AUTONUMBERING =
			"^([#]+)\\s*([0-9]+(?:\\.[0-9]+)*\\.?)?\\s*(.*?)\\s*(?:\\{\\s*([a-z\\-]+)\\s*\\})?\\s*$"; //$NON-NLS-1$

	private static final String SECTION_PATTERN_NO_AUTONUMBERING =
			"^([#]+)\\s*(.*?)\\s*(?:\\{\\s*([a-z\\-]+)\\s*\\})?\\s*$"; //$NON-NLS-1$

	private static final String SECTION_PATTERN_TITLE_EXTRACTOR =
			"^(?:[#]+)\\s*((?:[0-9]+(?:\\.[0-9]+)*\\.?)?\\s*.*?\\s*(?:\\{\\s*([a-z\\-]+)\\s*\\})?)\\s*$"; //$NON-NLS-1$

	private static final String SECTION_PATTERN_TITLE_EXTRACTOR_WITHOUT_MD_PREFIX =
			"^\\s*([0-9]+(?:\\.[0-9]+)*\\.?)\\s*(.*?)$"; //$NON-NLS-1$

	private static final Set<String> WARNING_LABELS = new TreeSet<>();

	private static final Set<String> DANGER_LABELS = new TreeSet<>();

	private IntegerRange outlineDepthRange = new IntegerRange(DEFAULT_OUTLINE_TOP_LEVEL, DEFAULT_OUTLINE_TOP_LEVEL);

	private boolean addLinkToOperationName = DEFAULT_ADD_LINK_TO_OPERATION_NAME;

	private boolean sectionNumbering = DEFAULT_SECTION_NUMBERING;

	private String sectionTitleFormat = DEFAULT_SECTION_TITLE_FORMAT;

	private String sectionNumberFormat = SectionNumber.DEFAULT_SECTION_NUMBER_FORMAT;

	private String outlineEntryWithNumberFormat = DEFAULT_OUTLINE_ENTRY_W_AUTONUMBERING;

	private String outlineEntryWithoutNumberFormat = DEFAULT_OUTLINE_ENTRY_WO_AUTONUMBERING;

	private String outlineStyleId = DEFAULT_OUTLINE_STYLE_ID;

	private boolean localFileReferenceValidation = true;

	private boolean remoteReferenceValidation = true;

	private boolean localImageReferenceValidation = true;

	private boolean transformMdToHtmlReferences = true;

	private boolean transformPureHtmlReferences = true;

	private boolean generateOutline = true;

	private String externalOutlineMarker;

	private boolean kramdown;

	static {
		for (final String label : Messages.WARNING.split(",")) { //$NON-NLS-1$
			WARNING_LABELS.add(label.trim().toLowerCase());
		}
		for (final String label : Messages.DANGER.split(",")) { //$NON-NLS-1$
			DANGER_LABELS.add(label.trim().toLowerCase());
		}
	}

	@Override
	@Inject
	public void setDocumentParser(SarlDocumentationParser parser) {
		super.setDocumentParser(parser);
		updateBlockFormatter();
	}

	@Override
	public void setGithubExtensionEnable(boolean enable) {
		super.setGithubExtensionEnable(enable);
		updateBlockFormatter();
	}

	private void updateBlockFormatter() {
		final Function2<String, String, String> formatter;
		if (isGithubExtensionEnable()) {
			formatter = SarlDocumentationParser.getFencedCodeBlockFormatter();
		} else {
			formatter = SarlDocumentationParser.getBasicCodeBlockFormatter();
		}
		getDocumentParser().setBlockCodeTemplate(formatter);
	}

	@Override
	public String extractPageTitle(String content) {
		final var sectionPattern = Pattern.compile(
				isAutoSectionNumbering() ? SECTION_PATTERN_AUTONUMBERING : SECTION_PATTERN_NO_AUTONUMBERING,
						Pattern.MULTILINE);
		final var matcher = sectionPattern.matcher(content);
		final var depthRange = getOutlineDepthRange();
		final int titleGroupId;
		if (isAutoSectionNumbering()) {
			titleGroupId = 3;
		} else {
			titleGroupId = 2;
		}
		while (matcher.find()) {
			final var prefix = matcher.group(1);
			final var clevel = prefix.length();
			if (clevel < depthRange.getStart()) {
				final var title = matcher.group(titleGroupId);
				if (!Strings.isEmpty(title)) {
					return title;
				}
			}
		}
		return null;
	}

	/** Replies the style identifier that should be used for rendering the outline.
	 *
	 * <p>If an identifier exists, the outline will be enclosing by an HTML div tag with
	 * the class and id attributes set to the value.
	 *
	 * @return the outline style identifier.
	 */
	public String getOutlineStyleId() {
		return this.outlineStyleId;
	}

	/** Change the style identifier that should be used for rendering the outline.
	 *
	 * <p>If an identifier exists, the outline will be enclosing by an HTML div tag with
	 * the class and id attributes set to the value.
	 *
	 * @param id the outline style identifier.
	 */
	public void setOutlineStyleId(String id) {
		this.outlineStyleId = id;
	}

	/** Replies if the references to the Markdown files should be transform to references to HTML pages.
	 *
	 * @return {@code true} if the references should be validated.
	 * @see #isPureHtmlReferenceTransformation()
	 */
	public boolean isMarkdownToHtmlReferenceTransformation() {
		return this.transformMdToHtmlReferences;
	}

	/** Change the flag that indicates if the references the Markdown files should be transform to references to HTML pages.
	 *
	 * @param transform {@code true} if the references should be validated.
	 * @see #setPureHtmlReferenceTransformation(boolean)
	 */
	public void setMarkdownToHtmlReferenceTransformation(boolean transform) {
		this.transformMdToHtmlReferences = transform;
	}

	/** Replies if the pure HTML references (in "a" tags) should be transform to references to HTML pages.
	 *
	 * @return {@code true} if the references should be validated.
	 * @see #isMarkdownToHtmlReferenceTransformation()
	 */
	public boolean isPureHtmlReferenceTransformation() {
		return this.transformPureHtmlReferences;
	}

	/** Change the flag that indicates if the pure html references should be transform to references to HTML pages.
	 *
	 * @param transform {@code true} if the references should be validated.
	 * @see #setMarkdownToHtmlReferenceTransformation(boolean)
	 */
	public void setPureHtmlReferenceTransformation(boolean transform) {
		this.transformPureHtmlReferences = transform;
	}

	/** Replies if the references to the local files should be validated.
	 *
	 * @return {@code true} if the references to the local files should be validated.
	 */
	public boolean isLocalFileReferenceValidation() {
		return this.localFileReferenceValidation;
	}

	/** Change the flag that indicates if the references to the local files should be validated.
	 *
	 * @param validate {@code true} if the references to the local files should be validated.
	 */
	public void setLocalFileReferenceValidation(boolean validate) {
		this.localFileReferenceValidation = validate;
	}

	/** Replies if the references to the remote Internet pages should be validated.
	 *
	 * @return {@code true} if the references to the local files should be validated.
	 */
	public boolean isRemoteReferenceValidation() {
		return this.remoteReferenceValidation;
	}

	/** Change the flag that indicates if the references to the remote Internet pages should be validated.
	 *
	 * @param validate {@code true} if the references to the remote Internet pages should be validated.
	 */
	public void setRemoteReferenceValidation(boolean validate) {
		this.remoteReferenceValidation = validate;
	}

	/** Replies if the references to the local images should be validated.
	 *
	 * @return {@code true} if the references to the local images should be validated.
	 */
	public boolean isLocalImageReferenceValidation() {
		return this.localImageReferenceValidation;
	}

	/** Change the flag that indicates if the references to the local images should be validated.
	 *
	 * @param validate {@code true} if the references to the local images should be validated.
	 */
	public void setLocalImageReferenceValidation(boolean validate) {
		this.localImageReferenceValidation = validate;
	}

	/** Change the formats to be applied to the outline entries.
	 *
	 * <p>The format must be compatible with {@link MessageFormat}.
	 *
	 * <p>If section auto-numbering is on,
	 * the first parameter {@code {0}} equals to the prefix,
	 * the second parameter {@code {1}} equals to the string representation of the section number,
	 * the third parameter {@code {2}} equals to the title text, and the fourth parameter
	 * {@code {3}} is the reference id of the section.
	 *
	 * <p>If section auto-numbering is off,
	 * the first parameter {@code {0}} equals to the prefix,
	 * the second parameter {@code {1}} equals to the title text, and the third parameter
	 * {@code {2}} is the reference id of the section.
	 *
	 * @param formatWithoutNumbers the format for the outline entries without section numbers.
	 * @param formatWithNumbers the format for the outline entries with section numbers.
	 */
	public void setOutlineEntryFormat(String formatWithoutNumbers, String formatWithNumbers) {
		if (!Strings.isEmpty(formatWithoutNumbers)) {
			this.outlineEntryWithoutNumberFormat = formatWithoutNumbers;
		}
		if (!Strings.isEmpty(formatWithNumbers)) {
			this.outlineEntryWithNumberFormat = formatWithNumbers;
		}
	}

	/** Replies the format to be applied to the outline entries.
	 *
	 * <p>The format must be compatible with {@link MessageFormat}.
	 *
	 * <p>If section auto-numbering is on,
	 * the first parameter {@code {0}} equals to the prefix,
	 * the second parameter {@code {1}} equals to the string representation of the section number,
	 * the third parameter {@code {2}} equals to the title text, and the fourth parameter
	 * {@code {3}} is the reference id of the section.
	 *
	 * <p>If section auto-numbering is off,
	 * the first parameter {@code {0}} equals to the prefix,
	 * the second parameter {@code {1}} equals to the title text, and the third parameter
	 * {@code {2}} is the reference id of the section.
	 *
	 * @return the format.
	 */
	public String getOutlineEntryFormat() {
		return isAutoSectionNumbering() ? this.outlineEntryWithNumberFormat : this.outlineEntryWithoutNumberFormat;
	}

	/** Change the format to be applied to the section titles.
	 *
	 * <p>The format must be compatible with {@link MessageFormat}, with
	 * the first parameter {@code {0}} equals to the Markdown prefix,
	 * the second parameter {@code {1}} equals to the string representation of the section number,
	 * the third parameter {@code {2}} equals to the title text, and the fourth parameter
	 * {@code {3}} is the identifier of the section.
	 *
	 * @param format the format.
	 */
	public void setSectionTitleFormat(String format) {
		if (!Strings.isEmpty(format)) {
			this.sectionTitleFormat = format;
		}
	}

	/** Replies the format to be applied to the section titles.
	 *
	 * <p>The format must be compatible with {@link MessageFormat}, with
	 * the first parameter {@code {0}} equals to the string representation of the section number,
	 * the second parameter {@code {1}} equals to the string representation of the section number,
	 * the third parameter {@code {2}} equals to the title text, and the fourth parameter
	 * {@code {3}} is the identifier of the section.
	 *
	 * @return the format.
	 */
	public String getSectionTitleFormat() {
		return this.sectionTitleFormat;
	}

	/** Change the format to be applied to the section numbers.
	 *
	 * <p>The format must be compatible with {@link MessageFormat}, with
	 * the first parameter {@code {0}} equals to the first part of the full section number, and
	 * the second parameter {@code {1}} equals to a single section number.
	 *
	 * @param format the format.
	 */
	public void setSectionNumberFormat(String format) {
		if (!Strings.isEmpty(format)) {
			this.sectionNumberFormat = format;
		}
	}

	/** Replies the format to be applied to the section numbers.
	 *
	 * <p>The format must be compatible with {@link MessageFormat}, with
	 * the first parameter {@code {0}} equals to the first part of the full section number, and
	 * the second parameter {@code {1}} equals to a single section number.
	 *
	 * @return the format.
	 */
	public String getSectionNumberFormat() {
		return this.sectionNumberFormat;
	}

	/** Set if the outline must be generated or not by this parser.
	 *
	 * @param enable {@code true} if the outline is automatically generated.
	 * @since 0.12
	 */
	public void setOutlineGeneration(boolean enable) {
		this.generateOutline = enable;
	}

	/** Replies if the outline must be generated or not by this parser.
	 *
	 * @return {@code true} if the outline is automatically generated.
	 * @since 0.12
	 */
	public boolean isOutlineGeneration() {
		return this.generateOutline;
	}

	/** Set the external marker that is used by a Markdown generator in order to
	 * automatically generate the outline.
	 *
	 * @param marker the external marker; or {@code null} if none.
	 * @since 0.12
	 */
	public void setOutlineExternalMarker(String marker) {
		this.externalOutlineMarker = marker;
	}

	/** Replies the external marker that is used by a Markdown generator in order to
	 * automatically generate the outline.
	 *
	 * @return the external marker; or {@code null} if none.
	 * @since 0.12
	 */
	public String getOutlineExternalMarker() {
		return this.externalOutlineMarker;
	}

	/** Change the flag that indicates if the bug fixes related to Kramdown are enabled.
	 * For example, the automatic section numbering is not working as expected into
	 * Kramdown; so that is should be fixed into our code.
	 *
	 * @param enable is {@code true} if the bug fixes are enabled.
	 * @since 0.12
	 */
	public void setKramdownFix(boolean enable) {
		this.kramdown = enable;
	}

	/** Replies if the bug fixes related to Kramdown are enabled.
	 *
	 * @return {@code true} if the bug fixes are enabled.
	 * @since 0.12
	 */
	public boolean isKramdownFix() {
		return this.kramdown;
	}

	/** Change the level at which the titles may appear in the outline.
	 *
	 * @param level the level, at least 1.
	 */
	public void setOutlineDepthRange(IntegerRange level) {
		if (level == null) {
			this.outlineDepthRange = new IntegerRange(DEFAULT_OUTLINE_TOP_LEVEL, DEFAULT_OUTLINE_TOP_LEVEL);
		} else {
			this.outlineDepthRange = level;
		}
	}

	/** Replies the level at which the titles may appear in the outline.
	 *
	 * @return the level, at least 1.
	 */
	public IntegerRange getOutlineDepthRange() {
		return this.outlineDepthRange;
	}

	/** Set if the sections are automatically numbered.
	 *
	 * @param enable {@code true} if the section are automatically numbered.
	 */
	public void setAutoSectionNumbering(boolean enable) {
		this.sectionNumbering = enable;
	}

	/** Replies if the sections are automatically numbered.
	 *
	 * @return {@code true} if the section are automatically numbered.
	 */
	public boolean isAutoSectionNumbering() {
		return this.sectionNumbering;
	}

	/** Chagne the flag for the creation of a hyperlink to the operation documentation
	 * to each generated operation name.
	 *
	 * @param enable {@code true} if the hyperlink is created.
	 */
	public void setAddLinkToOperationName(boolean enable) {
		this.addLinkToOperationName = enable;
	}

	/** Replies if a hyperlink to the operation documentation should be added to each generated operation name.
	 *
	 * @return {@code true} if the hyperlink is created.
	 */
	public boolean isAddLinkToOperationName() {
		return this.addLinkToOperationName;
	}

	/** Replies the hyperlink to the given operation.
	 *
	 * @param method the method.
	 * @return the link, or {@code null} if not found.
	 */
	@SuppressWarnings("static-method")
	protected URL findOperationLink(Method method) {
		return null;
	}

	@Override
	protected void preProcessingTransformation(CharSequence content, File inputFile, boolean validationOfInternalLinks) {
		Function<Method, String> formatter = null;
		if (isAddLinkToOperationName()) {
			formatter = it -> {
				final var url = findOperationLink(it);
				if (url != null) {
					final var name = new StringBuilder();
					name.append("[").append(it.getName()).append("]("); //$NON-NLS-1$//$NON-NLS-2$
					name.append(url.toExternalForm()).append(")"); //$NON-NLS-1$
					return name.toString();
				}
				return it.getName();
			};
		}
		ReflectExtensions.setDefaultMethodNameFormatter(formatter);
	}

	@Override
	protected String postProcessingTransformation(String content, boolean validationOfInternalLinks) {
		var result = updateOutline(content);
		final var references = validationOfInternalLinks ? extractReferencableElements(result) : null;
		result = transformMardownLinks(result, references);
		result = transformHtmlLinks(result, references);
		result = transformInformationNotes(result);
		return result;
	}

	/** Transform the information notes into the given Markdown text.
	 *
	 * @param text the content to parse.
	 * @return the updated text.
	 * @since 0.12
	 */
	@SuppressWarnings("static-method")
	protected String transformInformationNotes(String text) {
		final var info = Strings.emptyIfNull(System.getProperty(INFO_NOTE_PATTERN_PROPERTY));
		final var warning = Strings.emptyIfNull(System.getProperty(WARNING_NOTE_PATTERN_PROPERTY));
		final var danger = Strings.emptyIfNull(System.getProperty(DANGER_NOTE_PATTERN_PROPERTY));
		if (Strings.isEmpty(info) && Strings.isEmpty(warning) && Strings.isEmpty(danger)) {
			return text;
		}

		final var startPattern = Pattern.compile(MARKDOWN_INFORMATION_NOTE_PATTERN1);
		final var continuePattern = Pattern.compile(MARKDOWN_INFORMATION_NOTE_PATTERN2);
		final var result = new StringBuilder();
		String currentName = null;
		StringBuilder currentNote = null;
		for (final var line : text.split("\r*\n\r*")) { //$NON-NLS-1$
			String newLine = null;
			if (currentName == null) {
				final var matcher = startPattern.matcher(line);
				if (matcher.matches()) {
					currentName = matcher.group(1).trim();
					currentNote = new StringBuilder(matcher.group(2).trim());
				} else {
					currentNote = null;
					newLine = line;
				}
			} else {
				final var matcher = continuePattern.matcher(line);
				if (matcher.matches()) {
					assert currentNote != null;
					currentNote.append(" ").append(matcher.group(1).trim()); //$NON-NLS-1$
				} else {
					updateBuffer(result, currentName, currentNote, info, warning, danger);
					currentName = null;
					currentNote = null;
					newLine = line;
				}
			}
			updateBuffer(result, newLine);
		}
		updateBuffer(result, currentName, currentNote, info, warning, danger);
		return result.toString();
	}

	private static void updateBuffer(StringBuilder result, String name, StringBuilder note, String info, String warning, String danger) {
		if (!Strings.isEmpty(name) && note != null) {
			final var type = parseType(name);
			switch (type) {
			case 0:
				if (!Strings.isEmpty(info)) {
					var res = info.replaceAll(Pattern.quote("$1"), Matcher.quoteReplacement(name)); //$NON-NLS-1$
					res = res.replaceAll(Pattern.quote("$2"), Matcher.quoteReplacement(note.toString())); //$NON-NLS-1$
					updateBuffer(result, res);
				} else {
					updateBuffer(result, "> **_" + name + ":_** " + note); //$NON-NLS-1$ //$NON-NLS-2$
				}
				break;
			case 1:
				if (!Strings.isEmpty(warning)) {
					var res = warning.replaceAll(Pattern.quote("$1"), Matcher.quoteReplacement(name)); //$NON-NLS-1$
					res = res.replaceAll(Pattern.quote("$2"), Matcher.quoteReplacement(note.toString())); //$NON-NLS-1$
					updateBuffer(result, res);
				} else {
					updateBuffer(result, "> **_" + name + ":_** " + note); //$NON-NLS-1$ //$NON-NLS-2$
				}
				break;
			case 2:
				if (!Strings.isEmpty(danger)) {
					var res = danger.replaceAll(Pattern.quote("$1"), Matcher.quoteReplacement(name)); //$NON-NLS-1$
					res = res.replaceAll(Pattern.quote("$2"), Matcher.quoteReplacement(note.toString())); //$NON-NLS-1$
					updateBuffer(result, res);
				} else {
					updateBuffer(result, "> **_" + name + ":_** " + note); //$NON-NLS-1$ //$NON-NLS-2$
				}
				break;
			default:
				break;
			}
		}
	}

	private static void updateBuffer(StringBuilder result, String line) {
		if (line != null) {
			result.append(line).append("\n"); //$NON-NLS-1$
		}
	}

	private static int parseType(String name) {
		final var label = name.toLowerCase();
		if (WARNING_LABELS.contains(label)) {
			return 1;
		}
		if (DANGER_LABELS.contains(label)) {
			return 2;
		}
		return 0;
	}

	/** Extract all the referencable objects from the given content.
	 *
	 * @param text the content to parse.
	 * @return the referencables objects
	 */
	protected ReferenceContext extractReferencableElements(String text) {
		final var context = new ReferenceContext();

		// Visit the links and record the transformations
		final var options = new MutableDataSet();
		final var parser = Parser.builder(options).build();
		final var document = parser.parse(text);
		final var pattern = Pattern.compile(SECTION_PATTERN_AUTONUMBERING);
		var visitor = new NodeVisitor(
				new VisitHandler<>(Paragraph.class, it -> {
					final var paragraphText = it.getContentChars();
					final var matcher = pattern.matcher(paragraphText);
					if (matcher.find()) {
						final var number = matcher.group(2);
						final var title = matcher.group(3);
						final var key1 = computeHeaderId(number, title);
						final var key2 = computeHeaderId(null, title);
						context.registerSection(key1, key2, title);
					}
				}));
		visitor.visitChildren(document);

		final var pattern1 = Pattern.compile(SECTION_PATTERN_TITLE_EXTRACTOR_WITHOUT_MD_PREFIX);
		visitor = new NodeVisitor(
				new VisitHandler<>(Heading.class, it -> {
					var key = it.getAnchorRefId();
					var title = it.getText().toString();
					// Sometimes, the title already contains the section number.
					// It should be removed.
					final var matcher = pattern1.matcher(title);
					if (matcher.find()) {
						final var number = matcher.group(1);
						title = matcher.group(2);
						if (Strings.isEmpty(key)) {
							key = computeHeaderId(number, title);
						}
					}
					final var key2 = computeHeaderId(null, title);
					if (Strings.isEmpty(key)) {
						key = key2;
					}
					context.registerSection(key, key2, title);
				}));
		visitor.visitChildren(document);

		return context;
	}

	/** Apply link transformation on the HTML links.
	 *
	 * @param content the original content.
	 * @param references the references into the document.
	 * @return the result of the transformation.
	 */
	protected String transformHtmlLinks(String content, ReferenceContext references) {
		if (!isPureHtmlReferenceTransformation()) {
			return content;
		}

		// Prepare replacement data structures
		final var replacements = new TreeMap<String, String>();

		// Visit the links and record the transformations
		final var visitor = new org.jsoup.select.NodeVisitor() {
			@Override
			public void tail(org.jsoup.nodes.Node node, int depth) {
				//
			}

			@Override
			public void head(org.jsoup.nodes.Node node, int depth) {
				if (node instanceof Element tag) {
					if ("a".equals(tag.nodeName()) && tag.hasAttr("href")) { //$NON-NLS-1$ //$NON-NLS-2$
						final var href = tag.attr("href"); //$NON-NLS-1$
						if (!Strings.isEmpty(href)) {
							var url = FileSystem.convertStringToURL(href, true);
							url = transformURL(url, -1, references);
							if (url != null) {
								final var newUrl = convertURLToString(url);
								if (!Strings.isEmpty(newUrl) && !Objects.equals(href, newUrl)) {
									replacements.put(href, newUrl);
								}
							}
						}
					}
				}
			}
		};
		final var htmlDocument = Jsoup.parse(content);
		htmlDocument.traverse(visitor);

		// Apply the replacements
		if (!replacements.isEmpty()) {
			var buffer = content;
			for (final var entry : replacements.entrySet()) {
				final var source = entry.getKey();
				final var dest = entry.getValue();
				buffer = buffer.replaceAll(Pattern.quote(source), Matcher.quoteReplacement(dest));
			}
			return buffer;
		}
		return content;
	}

	/** Apply link transformation on the Markdown links.
	 *
	 * @param content the original content.
	 * @param references the references into the document.
	 * @return the result of the transformation.
	 */
	protected String transformMardownLinks(String content, ReferenceContext references) {
		if (!isMarkdownToHtmlReferenceTransformation()) {
			return content;
		}

		// Prepare replacement data structures
		final var replacements = new TreeMap<BasedSequence, String>((cmp1, cmp2) -> {
			final var cmp = Integer.compare(cmp2.getStartOffset(), cmp1.getStartOffset());
			if (cmp != 0) {
				return cmp;
			}
			return Integer.compare(cmp2.getEndOffset(), cmp1.getEndOffset());
		});

		// Visit the links and record the transformations
		final var options = new MutableDataSet();
		final var parser = Parser.builder(options).build();
		final var document = parser.parse(content);
		final var visitor = new NodeVisitor(
				new VisitHandler<>(Link.class, it -> {
					var url = FileSystem.convertStringToURL(it.getUrl().toString(), true);
					url = transformURL(url, it.getLineNumber(), references);
					if (url != null) {
						replacements.put(it.getUrl(), convertURLToString(url));
					}
				}));
		visitor.visitChildren(document);

		// Apply the replacements
		if (!replacements.isEmpty()) {
			final var buffer = new StringBuilder(content);
			for (final var entry : replacements.entrySet()) {
				final var seq = entry.getKey();
				buffer.replace(seq.getStartOffset(), seq.getEndOffset(), entry.getValue());
			}
			return buffer.toString();
		}
		return content;
	}

	/** Convert the given URL to its string representation that is compatible with Markdown document linking mechanism.
	 *
	 * @param url the URL to transform.
	 * @return the sting representation of the URL.
	 */
	static String convertURLToString(URL url) {
		if (URISchemeType.FILE.isURL(url)) {
			final var externalForm = new StringBuilder();
			externalForm.append(url.getPath());
			final var ref = url.getRef();
			if (!Strings.isEmpty(ref)) {
				externalForm.append("#").append(ref); //$NON-NLS-1$
			}
			return externalForm.toString();
		}
		return url.toExternalForm();
	}

	/** Transform an URL from Markdown format to HTML format.
	 *
	 * <p>Usually, the file extension ".md" is replaced by ".html".
	 *
	 * <p>This function replaces the anchor to the local reference with the correct one (modified by outline feature).
	 *
	 * @param link the link to transform.
	 * @param line the number of the line at which the link is located. If negative value, the line number is unknown.
	 * @param references the set of references from the local document.
	 * @return the result of the transformation. {@code null} if the link should not changed.
	 * @since 0.12
	 */
	protected URL transformURL(URL link, int line, ReferenceContext references) {
		if (URISchemeType.FILE.isURL(link)) {
			var filename = FileSystem.convertURLToFile(link);
			if (Strings.isEmpty(filename.getName())) {
				// This is a link to the local document.
				final var anchor = transformURLAnchor(filename, line, link.getRef(), references);
				final var url = FileSystemAddons.convertFileToURL(filename, true);
				if (!Strings.isEmpty(anchor)) {
					try {
						return new URI(url.toExternalForm() + "#" + anchor).toURL(); //$NON-NLS-1$
					} catch (Exception e) {
						//
					}
				}
				return url;
			}
			// This is a link to another document.
			final var extension = FileSystem.extension(filename);
			if (isMarkdownFileExtension(extension)) {
				filename = FileSystem.replaceExtension(filename, ".html"); //$NON-NLS-1$
				final var anchor = transformURLAnchor(filename, line, link.getRef(), null);
				final var url = FileSystemAddons.convertFileToURL(filename, true);
				if (!Strings.isEmpty(anchor)) {
					try {
						return new URI(url.toExternalForm() + "#" + anchor).toURL(); //$NON-NLS-1$
					} catch (Exception e) {
						//
					}
				}
				return url;
			}
		}
		return null;
	}

	/** Transform the anchor of an URL from Markdown format to HTML format.
	 *
	 * @param file the linked file.
	 * @param line the number of the line at which the anchor is located. If negative value, the line number is unknown.
	 * @param anchor the anchor to transform.
