All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.sarl.maven.docs.markdown.MarkdownParser Maven / Gradle / Ivy

There is a newer version: 0.12.0
Show newest version
/*
 * $Id: io/sarl/maven/docs/markdown/MarkdownParser.java v0.10.0 2019-10-26 17:20:53$
 *
 * SARL is an general-purpose agent programming language.
 * More details on http://www.sarl.io
 *
 * Copyright (C) 2014-2019 the original authors or 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.maven.docs.markdown;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
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.Map;
import java.util.Map.Entry;
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 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 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.Document;
import org.jsoup.nodes.Element;

import io.sarl.maven.docs.bugfixes.FileSystemAddons;
import io.sarl.maven.docs.parser.AbstractMarkerLanguageParser;
import io.sarl.maven.docs.parser.DynamicValidationComponent;
import io.sarl.maven.docs.parser.DynamicValidationContext;
import io.sarl.maven.docs.parser.SarlDocumentationParser;
import io.sarl.maven.docs.parser.SectionNumber;
import io.sarl.maven.docs.testing.ReflectExtensions;

/** Markdown parser.
 *
 * @author Stéphane Galland
 * @version 0.10.0 2019-10-26 17:20:53
 * @mavengroupid io.sarl.maven
 * @mavenartifactid io.sarl.maven.docs.generator
 * @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$

	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 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;

	@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 formatter;
		if (isGithubExtensionEnable()) {
			formatter = SarlDocumentationParser.getFencedCodeBlockFormatter();
		} else {
			formatter = SarlDocumentationParser.getBasicCodeBlockFormatter();
		}
		getDocumentParser().setBlockCodeTemplate(formatter);
	}

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

	/** Replies the style identifier that should be used for rendering the outline.
	 *
	 * 

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

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

The format must be compatible with {@link MessageFormat}. * *

If section auto-numbering is on, * the first parameter {0} equals to the prefix, * the second parameter {1} equals to the string representation of the section number, * the third parameter {2} equals to the title text, and the fourth parameter * {3} is the reference id of the section. * *

If section auto-numbering is off, * the first parameter {0} equals to the prefix, * the second parameter {1} equals to the title text, and the third parameter * {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. * *

The format must be compatible with {@link MessageFormat}. * *

If section auto-numbering is on, * the first parameter {0} equals to the prefix, * the second parameter {1} equals to the string representation of the section number, * the third parameter {2} equals to the title text, and the fourth parameter * {3} is the reference id of the section. * *

If section auto-numbering is off, * the first parameter {0} equals to the prefix, * the second parameter {1} equals to the title text, and the third parameter * {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. * *

The format must be compatible with {@link MessageFormat}, with * the first parameter {0} equals to the Markdown prefix, * the second parameter {1} equals to the string representation of the section number, * the third parameter {2} equals to the title text, and the fourth parameter * {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. * *

The format must be compatible with {@link MessageFormat}, with * the first parameter {0} equals to the string representation of the section number, * the second parameter {1} equals to the string representation of the section number, * the third parameter {2} equals to the title text, and the fourth parameter * {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. * *

The format must be compatible with {@link MessageFormat}, with * the first parameter {0} equals to the first part of the full section number, and * the second parameter {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. * *

The format must be compatible with {@link MessageFormat}, with * the first parameter {0} equals to the first part of the full section number, and * the second parameter {1} equals to a single section number. * * @return the format. */ public String getSectionNumberFormat() { return this.sectionNumberFormat; } /** 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 formatter = null; if (isAddLinkToOperationName()) { formatter = it -> { final URL url = findOperationLink(it); if (url != null) { final StringBuilder 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.setDefaultNameFormatter(formatter); } @Override protected String postProcessingTransformation(String content, boolean validationOfInternalLinks) { String result = updateOutline(content); final ReferenceContext references = validationOfInternalLinks ? extractReferencableElements(result) : null; result = transformMardownLinks(result, references); result = transformHtmlLinks(result, references); return result; } /** Extract all the referencable objects from the given content. * * @param text the content to parse. * @return the referencables objects */ @SuppressWarnings("static-method") protected ReferenceContext extractReferencableElements(String text) { final ReferenceContext context = new ReferenceContext(); // Visit the links and record the transformations final MutableDataSet options = new MutableDataSet(); final Parser parser = Parser.builder(options).build(); final Node document = parser.parse(text); final Pattern pattern = Pattern.compile(SECTION_PATTERN_AUTONUMBERING); NodeVisitor visitor = new NodeVisitor( new VisitHandler<>(Paragraph.class, it -> { final Matcher matcher = pattern.matcher(it.getContentChars()); if (matcher.find()) { final String number = matcher.group(2); final String title = matcher.group(3); final String key1 = computeHeaderId(number, title); final String key2 = computeHeaderId(null, title); context.registerSection(key1, key2, title); } })); visitor.visitChildren(document); visitor = new NodeVisitor( new VisitHandler<>(Heading.class, it -> { String key = it.getAnchorRefId(); final String title = it.getAnchorRefText(); final String 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 Map replacements = new TreeMap<>(); // Visit the links and record the transformations final org.jsoup.select.NodeVisitor visitor = new org.jsoup.select.NodeVisitor() { @Override public void tail(org.jsoup.nodes.Node node, int index) { // } @Override public void head(org.jsoup.nodes.Node node, int index) { if (node instanceof Element) { final Element tag = (Element) node; if ("a".equals(tag.nodeName()) && tag.hasAttr("href")) { //$NON-NLS-1$ //$NON-NLS-2$ final String href = tag.attr("href"); //$NON-NLS-1$ if (!Strings.isEmpty(href)) { URL url = FileSystem.convertStringToURL(href, true); url = transformURL(url, references); if (url != null) { replacements.put(href, convertURLToString(url)); } } } } } }; final Document htmlDocument = Jsoup.parse(content); htmlDocument.traverse(visitor); // Apply the replacements if (!replacements.isEmpty()) { String buffer = content; for (final Entry entry : replacements.entrySet()) { final String source = entry.getKey(); final String 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 Map replacements = new TreeMap<>((cmp1, cmp2) -> { final int 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 MutableDataSet options = new MutableDataSet(); final Parser parser = Parser.builder(options).build(); final Node document = parser.parse(content); final NodeVisitor visitor = new NodeVisitor( new VisitHandler<>(Link.class, it -> { URL url = FileSystem.convertStringToURL(it.getUrl().toString(), true); url = transformURL(url, references); if (url != null) { replacements.put(it.getUrl(), convertURLToString(url)); } })); visitor.visitChildren(document); // Apply the replacements if (!replacements.isEmpty()) { final StringBuilder buffer = new StringBuilder(content); for (final Entry entry : replacements.entrySet()) { final BasedSequence 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 StringBuilder externalForm = new StringBuilder(); externalForm.append(url.getPath()); final String 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. * *

Usually, the file extension ".md" is replaced by ".html". * *

This function replaces the anchor to the local reference with the correct one (modified by outline feature). * * @param link the link to transform. * @param references the set of references from the local document. * @return the result of the transformation. {@code null} if the link should not changed. */ protected URL transformURL(URL link, ReferenceContext references) { if (URISchemeType.FILE.isURL(link)) { File filename = FileSystem.convertURLToFile(link); if (Strings.isEmpty(filename.getName())) { // This is a link to the local document. final String anchor = transformURLAnchor(filename, link.getRef(), references); final URL url = FileSystemAddons.convertFileToURL(filename, true); if (!Strings.isEmpty(anchor)) { try { return new URL(url.toExternalForm() + "#" + anchor); //$NON-NLS-1$ } catch (MalformedURLException e) { // } } return url; } // This is a link to another document. final String extension = FileSystem.extension(filename); if (isMarkdownFileExtension(extension)) { filename = FileSystem.replaceExtension(filename, ".html"); //$NON-NLS-1$ final String anchor = transformURLAnchor(filename, link.getRef(), null); final URL url = FileSystemAddons.convertFileToURL(filename, true); if (!Strings.isEmpty(anchor)) { try { return new URL(url.toExternalForm() + "#" + anchor); //$NON-NLS-1$ } catch (MalformedURLException e) { // } } return url; } } return null; } /** Transform the anchor of an URL from Markdown format to HTML format. * * @param file the linked file. * @param anchor the anchor to transform. * @param references the set of references from the local document, or {@code null}. * @return the result of the transformation. * @since 0.7 */ @SuppressWarnings("static-method") protected String transformURLAnchor(File file, String anchor, ReferenceContext references) { String anc = anchor; if (references != null) { anc = references.validateAnchor(anc); } return anc; } /** Replies if the given extension is for Markdown file. * * @param extension the extension to test. * @return {@code true} if the extension is for a Markdown file. */ public static boolean isMarkdownFileExtension(String extension) { for (final String ext : MARKDOWN_FILE_EXTENSIONS) { if (Strings.equal(ext, extension)) { return true; } } return false; } /** Update the outline tags. * * @param content the content with outline tag. * @return the content with expended outline. */ @SuppressWarnings({"checkstyle:npathcomplexity", "checkstyle:cyclomaticcomplexity"}) protected String updateOutline(String content) { final Pattern sectionPattern = Pattern.compile( isAutoSectionNumbering() ? SECTION_PATTERN_AUTONUMBERING : SECTION_PATTERN_NO_AUTONUMBERING, Pattern.MULTILINE); final Matcher matcher = sectionPattern.matcher(content); final Set identifiers = new TreeSet<>(); final StringBuilder outline = new StringBuilder(); outline.append("\n"); //$NON-NLS-1$ final String outlineStyleId = getOutlineStyleId(); final boolean styledOutline = !Strings.isEmpty(outlineStyleId); if (styledOutline) { outline.append("

    \n\n"); //$NON-NLS-1$ } final IntegerRange outlineDepthRange = getOutlineDepthRange(); final StringBuffer output; final SectionNumber sections; final int titleGroupId; if (isAutoSectionNumbering()) { output = new StringBuffer(); sections = new SectionNumber(); titleGroupId = 3; } else { output = null; sections = null; titleGroupId = 2; } int prevLevel = 0; int nbOpened = 0; while (matcher.find()) { final String prefix = matcher.group(1); final int clevel = prefix.length(); if (outlineDepthRange.contains(clevel)) { final int relLevel = clevel - outlineDepthRange.getStart(); final String title = matcher.group(titleGroupId); String sectionId = matcher.group(titleGroupId + 1); if (output != null) { assert sections != null; String sectionNumber = matcher.group(2); if (!Strings.isEmpty(sectionNumber)) { sections.setFromString(sectionNumber, relLevel + 1); } else { sections.increment(relLevel + 1); } sectionNumber = formatSectionNumber(sections); if (Strings.isEmpty(sectionId)) { sectionId = computeHeaderId(sectionNumber, title); if (!identifiers.add(sectionId)) { int idNum = 1; String nbId = sectionId + "-" + idNum; //$NON-NLS-1$ while (!identifiers.add(nbId)) { ++idNum; nbId = sectionId + "-" + idNum; //$NON-NLS-1$ } sectionId = nbId; } } matcher.appendReplacement(output, formatSectionTitle(prefix, sectionNumber, title, sectionId)); if (styledOutline && (relLevel > 0 || prevLevel > 0) && relLevel != prevLevel) { if (relLevel > prevLevel) { for (int i = prevLevel; i < relLevel; ++i) { outline.append("
      \n"); //$NON-NLS-1$ ++nbOpened; } } else { for (int i = relLevel; i < prevLevel; ++i) { outline.append("
    \n"); //$NON-NLS-1$ --nbOpened; } } } addOutlineEntry(outline, relLevel + 1, sectionNumber, title, sectionId, styledOutline); } else { if (Strings.isEmpty(sectionId)) { sectionId = computeHeaderId(null, title); if (!identifiers.add(sectionId)) { int idNum = 1; String nbId = sectionId + "-" + idNum; //$NON-NLS-1$ while (!identifiers.add(nbId)) { ++idNum; nbId = sectionId + "-" + idNum; //$NON-NLS-1$ } sectionId = nbId; } } addOutlineEntry(outline, relLevel + 1, null, title, sectionId, styledOutline); } prevLevel = relLevel; } } final String newContent; if (output != null) { matcher.appendTail(output); newContent = output.toString(); } else { newContent = content; } outline.append("\n"); //$NON-NLS-1$ if (styledOutline) { for (int i = 0; i <= nbOpened; ++i) { outline.append("
\n"); //$NON-NLS-1$ } } final String outlineTag = getDocumentParser().getOutlineOutputTag(); return newContent.replaceAll(Pattern.quote(outlineTag), outline.toString()); } /** Create the id of a section header. * *

The ID format follows the ReadCarpet standards. * * @param headerNumber the number of the header, or {@code null}. * @param headerText the section header text. * @return the identifier. */ public static String computeHeaderId(String headerNumber, String headerText) { final String fullText = Strings.emptyIfNull(headerNumber) + " " + Strings.emptyIfNull(headerText); //$NON-NLS-1$ return computeHeaderId(fullText); } /** Create the id of a section header. * *

The ID format follows the ReadCarpet standards. * * @param header the section header text. * @return the identifier. */ public static String computeHeaderId(String header) { String id = header.replaceAll("[^a-zA-Z0-9]+", "-"); //$NON-NLS-1$ //$NON-NLS-2$ id = id.toLowerCase(); id = id.replaceFirst("^[^a-zA-Z0-9]+", ""); //$NON-NLS-1$ //$NON-NLS-2$ id = id.replaceFirst("[^a-zA-Z0-9]+$", ""); //$NON-NLS-1$ //$NON-NLS-2$ if (Strings.isEmpty(id)) { return "section"; //$NON-NLS-1$ } return id; } /** Update the outline entry. * * @param outline the outline. * @param level the depth level in the outline. * @param sectionNumber the auto-computed section number, or {@code null} if no auto-computed number. * @param title the title of the section. * @param sectionId the identifier of the section. * @param htmlOutput indicates if the output should be HTML or not. */ protected void addOutlineEntry(StringBuilder outline, int level, String sectionNumber, String title, String sectionId, boolean htmlOutput) { if (htmlOutput) { indent(outline, level - 1, " "); //$NON-NLS-1$ outline.append("

  • "); //$NON-NLS-1$ if (isAutoSectionNumbering() && !Strings.isEmpty(sectionNumber)) { outline.append(sectionNumber).append(". "); //$NON-NLS-1$ } outline.append(title); outline.append("
  • "); //$NON-NLS-1$ } else { final String prefix = "*"; //$NON-NLS-1$ final String entry; outline.append("> "); //$NON-NLS-1$ indent(outline, level - 1, "\t"); //$NON-NLS-1$ if (isAutoSectionNumbering()) { entry = MessageFormat.format(getOutlineEntryFormat(), prefix, Strings.emptyIfNull(sectionNumber), title, sectionId); } else { entry = MessageFormat.format(getOutlineEntryFormat(), prefix, title, sectionId); } outline.append(entry); } outline.append("\n"); //$NON-NLS-1$ } /** Format the section numbers. * * @param numbers the section numbers, level per level. * @return the formatted section number. */ protected String formatSectionNumber(SectionNumber numbers) { return numbers.toString(getSectionNumberFormat()); } /** Format the section title. * * @param prefix the Markdown prefix. * @param sectionNumber the section number. * @param title the section title. * @param sectionId the identifier of the section. * @return the formatted section title. */ protected String formatSectionTitle(String prefix, String sectionNumber, String title, String sectionId) { return MessageFormat.format(getSectionTitleFormat(), prefix, sectionNumber, title, sectionId) + "\n"; //$NON-NLS-1$ } /** Create indentation in the given buffer. * * @param buffer the buffer. * @param number the number of identations. * @param character the string for a single indentation. */ protected static void indent(StringBuilder buffer, int number, String character) { for (int i = 0; i < number; ++i) { buffer.append(character); } } @Override protected List getSpecificValidationComponents(String text, File inputFile, File rootFolder, DynamicValidationContext context) { final MutableDataSet options = new MutableDataSet(); final Parser parser = Parser.builder(options).build(); final Node document = parser.parse(text); final List validators = new ArrayList<>(); File cfile; try { cfile = FileSystem.makeRelative(inputFile, rootFolder); } catch (IOException exception) { cfile = inputFile.getParentFile(); } final File currentFile = cfile; final NodeVisitor visitor = new NodeVisitor( new VisitHandler<>(Link.class, it -> { final Iterable components = createValidatorComponents(it, currentFile, context); for (final DynamicValidationComponent component : components) { validators.add(component); } }), new VisitHandler<>(Image.class, it -> { final Iterable components = createValidatorComponents(it, currentFile, context); for (final DynamicValidationComponent component : components) { validators.add(component); } })); visitor.visitChildren(document); return validators; } /** Compute the number of lines for reaching the given node. * * @param node the node. * @return the line number for the node. */ protected static int computeLineNo(Node node) { final int offset = node.getStartOffset(); final BasedSequence seq = node.getDocument().getChars(); int tmpOffset = seq.endOfLine(0); int lineno = 1; while (tmpOffset < offset) { ++lineno; tmpOffset = seq.endOfLineAnyEOL(tmpOffset + seq.eolLength(tmpOffset)); } return lineno; } /** Create a validation component for an image reference. * * @param it the image reference. * @param currentFile the current file. * @param context the validation context. * @return the validation components. */ protected Iterable createValidatorComponents(Image it, File currentFile, DynamicValidationContext context) { final Collection components = new ArrayList<>(); if (isLocalImageReferenceValidation()) { final int lineno = computeLineNo(it); final URL url = FileSystem.convertStringToURL(it.getUrl().toString(), true); if (URISchemeType.FILE.isURL(url)) { final DynamicValidationComponent component = createLocalImageValidatorComponent( it, url, lineno, currentFile, context); if (component != null) { components.add(component); } } } return components; } /** Create a validation component for an hyper reference. * * @param it the hyper reference. * @param currentFile the current file. * @param context the validation context. * @return the validation components. */ protected Iterable createValidatorComponents(Link it, File currentFile, DynamicValidationContext context) { final Collection components = new ArrayList<>(); if (isLocalFileReferenceValidation() || isRemoteReferenceValidation()) { final int lineno = computeLineNo(it); final URL url = FileSystem.convertStringToURL(it.getUrl().toString(), true); if (URISchemeType.HTTP.isURL(url) || URISchemeType.HTTPS.isURL(url) || URISchemeType.FTP.isURL(url)) { if (isRemoteReferenceValidation()) { final Collection newComponents = createRemoteReferenceValidatorComponents( it, url, lineno, currentFile, context); if (newComponents != null && !newComponents.isEmpty()) { components.addAll(newComponents); } } } else if (URISchemeType.FILE.isURL(url)) { if (isLocalFileReferenceValidation()) { final Collection newComponents = createLocalFileValidatorComponents( it, url, lineno, currentFile, context); if (newComponents != null && !newComponents.isEmpty()) { components.addAll(newComponents); } } } } return components; } /** Create a validation component for a reference to a local image. * * @param it the reference. * @param url the parsed URL of the link. * @param lineno the position of the link into the Markdown file. * @param currentFile the current file. * @param context the validation context. * @return the validation component. */ @SuppressWarnings("static-method") protected DynamicValidationComponent createLocalImageValidatorComponent(Image it, URL url, int lineno, File currentFile, DynamicValidationContext context) { File fn = FileSystem.convertURLToFile(url); if (!fn.isAbsolute()) { fn = FileSystem.join(currentFile.getParentFile(), fn); } final File filename = fn; return new DynamicValidationComponent() { @Override public String functionName() { return "Image_reference_test_" + lineno + "_"; //$NON-NLS-1$ //$NON-NLS-2$ } @Override public void generateValidationCode(ITreeAppendable it) { context.appendFileExistenceTest(it, filename, Messages.MarkdownParser_0); } }; } /** Create a validation component for an hyper reference to a local file. * * @param it the hyper reference. * @param url the parsed URL of the link. * @param lineno the position of the link into the Markdown file. * @param currentFile the current File. * @param context the validation context. * @return the validation component. */ @SuppressWarnings("static-method") protected Collection createLocalFileValidatorComponents(Link it, URL url, int lineno, File currentFile, DynamicValidationContext context) { File fn = FileSystem.convertURLToFile(url); if (Strings.isEmpty(fn.getName())) { // Special case: the URL should point to a anchor in the current document. final String linkRef = url.getRef(); if (!Strings.isEmpty(linkRef)) { return Arrays.asList(new DynamicValidationComponent() { @Override public String functionName() { return "Documentation_reference_anchor_test_" + lineno + "_"; //$NON-NLS-1$ //$NON-NLS-2$ } @Override public void generateValidationCode(ITreeAppendable it) { context.setTempResourceRoots(null); context.appendTitleAnchorExistenceTest(it, currentFile, url.getRef(), SECTION_PATTERN_TITLE_EXTRACTOR, Iterables.concat( Arrays.asList(MARKDOWN_FILE_EXTENSIONS), Arrays.asList(HTML_FILE_EXTENSIONS))); } }); } // No need to validate the current file's existency and anchor. return null; } if (!fn.isAbsolute()) { fn = FileSystem.join(currentFile.getParentFile(), fn); } final File filename = fn; final String extension = FileSystem.extension(filename); if (isMarkdownFileExtension(extension) || isHtmlFileExtension(extension)) { // Special case: the file may be a HTML or a Markdown file. final DynamicValidationComponent existence = new DynamicValidationComponent() { @Override public String functionName() { return "Documentation_reference_test_" + lineno + "_"; //$NON-NLS-1$ //$NON-NLS-2$ } @Override public void generateValidationCode(ITreeAppendable it) { context.setTempResourceRoots(context.getSourceRoots()); context.appendFileExistenceTest(it, filename, Messages.MarkdownParser_1, Iterables.concat( Arrays.asList(MARKDOWN_FILE_EXTENSIONS), Arrays.asList(HTML_FILE_EXTENSIONS))); } }; if (!Strings.isEmpty(url.getRef())) { final DynamicValidationComponent refValidity = new DynamicValidationComponent() { @Override public String functionName() { return "Documentation_reference_anchor_test_" + lineno + "_"; //$NON-NLS-1$ //$NON-NLS-2$ } @Override public void generateValidationCode(ITreeAppendable it) { context.setTempResourceRoots(null); context.appendTitleAnchorExistenceTest(it, filename, url.getRef(), SECTION_PATTERN_TITLE_EXTRACTOR, Iterables.concat( Arrays.asList(MARKDOWN_FILE_EXTENSIONS), Arrays.asList(HTML_FILE_EXTENSIONS))); } }; return Arrays.asList(existence, refValidity); } return Collections.singleton(existence); } return Arrays.asList(new DynamicValidationComponent() { @Override public String functionName() { return "File_reference_test_" + lineno + "_"; //$NON-NLS-1$ //$NON-NLS-2$ } @Override public void generateValidationCode(ITreeAppendable it) { context.appendFileExistenceTest(it, filename, Messages.MarkdownParser_1); } }); } /** Create a validation component for an hyper reference to a remote Internet page. * * @param it the hyper reference. * @param url the parsed URL of the link. * @param lineno the position of the link into the Markdown file. * @param currentFile the current file. * @param context the validation context. * @return the validation component. */ @SuppressWarnings("static-method") protected Collection createRemoteReferenceValidatorComponents(Link it, URL url, int lineno, File currentFile, DynamicValidationContext context) { return Collections.singleton(new DynamicValidationComponent() { @Override public String functionName() { return "Web_reference_test_" + lineno + "_"; //$NON-NLS-1$ //$NON-NLS-2$ } @Override public void generateValidationCode(ITreeAppendable it) { it.append("assertURLAccessibility(").append(Integer.toString(lineno)); //$NON-NLS-1$ it.append(", new "); //$NON-NLS-1$ it.append(URL.class).append("(\""); //$NON-NLS-1$ it.append(Strings.convertToJavaString(url.toExternalForm())); it.append("\"));"); //$NON-NLS-1$ } }); } }




    © 2015 - 2025 Weber Informatics LLC | Privacy Policy