From eec315ac479eef2b6fc7c9f4bdb6e4b19fa8d070 Mon Sep 17 00:00:00 2001 From: Fred Bricon Date: Wed, 19 Jun 2019 09:29:37 -0400 Subject: [PATCH] Returns markdown formatted hovers. Fixed issue with confusion between html and already markdown documentation. Wrote unit tests Fixes #245 Signed-off-by: Nikolas Komonen --- org.eclipse.lsp4xml/pom.xml | 10 +++ .../eclipse/lsp4xml/dom/parser/Constants.java | 2 + .../ContentModelHoverParticipant.java | 13 ++-- .../xsd/contentmodel/XSDAnnotationModel.java | 31 +++++--- .../lsp4xml/utils/MarkdownConverter.java | 76 +++++++++++++++++++ .../lsp4xml/utils/MarkdownConverterTest.java | 51 +++++++++++++ 6 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/utils/MarkdownConverter.java create mode 100644 org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/utils/MarkdownConverterTest.java diff --git a/org.eclipse.lsp4xml/pom.xml b/org.eclipse.lsp4xml/pom.xml index ac86881e33..fb095d0345 100644 --- a/org.eclipse.lsp4xml/pom.xml +++ b/org.eclipse.lsp4xml/pom.xml @@ -106,6 +106,16 @@ xml-apis 1.4.01 + + com.kotcrab.remark + remark + 1.0.0 + + + org.jsoup + jsoup + 1.9.2 + xml-resolver xml-resolver diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/parser/Constants.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/parser/Constants.java index 4f712cd5c3..4435e3f2bb 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/parser/Constants.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/parser/Constants.java @@ -95,4 +95,6 @@ public class Constants { public static final Pattern DOCTYPE_NAME = Pattern.compile("^[_:\\w][_:\\w-.\\d]*"); + public static final Pattern DOCUMENTATION_CONTENT = Pattern.compile(".*<[\\S]+:?documentation[^\\>]*>(.*)<\\/[\\S]+:?documentation>.*"); + } diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/extensions/contentmodel/participants/ContentModelHoverParticipant.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/extensions/contentmodel/participants/ContentModelHoverParticipant.java index e73830c564..37453c616e 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/extensions/contentmodel/participants/ContentModelHoverParticipant.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/extensions/contentmodel/participants/ContentModelHoverParticipant.java @@ -22,6 +22,7 @@ import org.eclipse.lsp4xml.services.extensions.HoverParticipantAdapter; import org.eclipse.lsp4xml.services.extensions.IHoverRequest; import org.eclipse.lsp4xml.uriresolver.CacheResourceDownloadingException; +import org.eclipse.lsp4xml.utils.MarkdownConverter; /** * Extension to support XML hover based on content model (XML Schema completion, @@ -38,9 +39,10 @@ public Hover onTag(IHoverRequest hoverRequest) throws Exception { if (cmElement != null) { String doc = cmElement.getDocumentation(); if (doc != null && doc.length() > 0) { + String markdown = MarkdownConverter.convert(doc); MarkupContent content = new MarkupContent(); - content.setKind(MarkupKind.PLAINTEXT); - content.setValue(doc); + content.setKind(MarkupKind.MARKDOWN); + content.setValue(markdown); return new Hover(content, hoverRequest.getTagRange()); } } @@ -100,9 +102,10 @@ public Hover onAttributeValue(IHoverRequest hoverRequest) throws Exception { if (cmAttribute != null) { String doc = cmAttribute.getValueDocumentation(attributeValue); if (doc != null && doc.length() > 0) { + String markdown = MarkdownConverter.convert(doc); MarkupContent content = new MarkupContent(); - content.setKind(MarkupKind.PLAINTEXT); - content.setValue(doc); + content.setKind(MarkupKind.MARKDOWN); + content.setValue(markdown); return new Hover(content); } } @@ -116,7 +119,7 @@ public Hover onAttributeValue(IHoverRequest hoverRequest) throws Exception { private Hover getCacheWarningHover(CacheResourceDownloadingException e) { // Here cache is enabled and some XML Schema, DTD, etc are loading MarkupContent content = new MarkupContent(); - content.setKind(MarkupKind.PLAINTEXT); + content.setKind(MarkupKind.MARKDOWN); content.setValue("Cannot process " + (e.isDTD() ? "DTD" : "XML Schema") + " hover: " + e.getMessage()); return new Hover(content); } diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/extensions/xsd/contentmodel/XSDAnnotationModel.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/extensions/xsd/contentmodel/XSDAnnotationModel.java index de77477cf6..3a32e1165d 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/extensions/xsd/contentmodel/XSDAnnotationModel.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/extensions/xsd/contentmodel/XSDAnnotationModel.java @@ -10,9 +10,11 @@ */ package org.eclipse.lsp4xml.extensions.xsd.contentmodel; +import static org.eclipse.lsp4xml.dom.parser.Constants.DOCUMENTATION_CONTENT; import static org.eclipse.lsp4xml.utils.StringUtils.normalizeSpace; import java.io.StringReader; +import java.util.regex.Matcher; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; @@ -22,6 +24,7 @@ import org.apache.xerces.xs.XSMultiValueFacet; import org.apache.xerces.xs.XSObjectList; import org.apache.xerces.xs.datatypes.ObjectList; +import org.eclipse.lsp4xml.utils.StringUtils; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -60,26 +63,25 @@ public static String getDocumentation(XSObjectList annotations, String value) { StringBuilder doc = new StringBuilder(); for (Object object : annotations) { XSAnnotation annotation = null; - if(object instanceof XSMultiValueFacet && value != null) { + if (object instanceof XSMultiValueFacet && value != null) { XSMultiValueFacet multiValueFacet = (XSMultiValueFacet) object; ObjectList enumerationValues = multiValueFacet.getEnumerationValues(); XSObjectList annotationValues = multiValueFacet.getAnnotations(); for (int i = 0; i < enumerationValues.getLength(); i++) { Object enumValue = enumerationValues.get(i); - //Assuming always ValidatedInfo + // Assuming always ValidatedInfo String enumString = ((ValidatedInfo) enumValue).stringValue(); - - if(value.equals(enumString)) { + + if (value.equals(enumString)) { annotation = (XSAnnotation) annotationValues.get(i); break; } } - } - else if(object instanceof XSAnnotation) { + } else if (object instanceof XSAnnotation) { annotation = (XSAnnotation) object; } - + XSDAnnotationModel annotationModel = XSDAnnotationModel.load(annotation); if (annotationModel != null) { if (annotationModel.getAppInfo() != null) { @@ -88,6 +90,10 @@ else if(object instanceof XSAnnotation) { if (annotationModel.getDocumentation() != null) { doc.append(annotationModel.getDocumentation()); } + String annotationString = annotation.getAnnotationString(); + if(!StringUtils.isEmpty(annotationString)) { + doc.append(getDocumentation(annotationString)); + } } } return doc.toString(); @@ -122,8 +128,7 @@ public XSDAnnotationModel getModel() { } @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) - throws SAXException { + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); if (qName.endsWith(DOCUMENTATION_ELEMENT) || qName.endsWith(APPINFO_ELEMENT)) { current = new StringBuilder(); @@ -153,4 +158,12 @@ public void characters(char[] ch, int start, int length) throws SAXException { } + public static String getDocumentation(String xml) { + Matcher m = DOCUMENTATION_CONTENT.matcher(xml); + if(m.find()) { + return m.group(1); + } + return null; + } + } diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/utils/MarkdownConverter.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/utils/MarkdownConverter.java new file mode 100644 index 0000000000..0e35f32461 --- /dev/null +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/utils/MarkdownConverter.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2016-2017 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + *******************************************************************************/ +package org.eclipse.lsp4xml.utils; + +import java.lang.reflect.Field; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +import com.overzealous.remark.Options; +import com.overzealous.remark.Options.Tables; +import com.overzealous.remark.Remark; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.jsoup.safety.Cleaner; +import org.jsoup.safety.Whitelist; + +/** + * Converts HTML content into Markdown equivalent. + * + * @author Fred Bricon + */ +public class MarkdownConverter { + + private static final Logger LOGGER = Logger.getLogger(MarkdownConverter.class.getName()); + + private static Remark remark; + + //Pattern looking for any form of tag eg: + private static final Pattern htmlPattern = Pattern.compile("[^`]*<[a-z][\\s\\S]*>[^`]*"); + + private MarkdownConverter(){ + //no public instanciation + } + + static { + Options options = new Options(); + options.tables = Tables.CONVERT_TO_CODE_BLOCK; + options.hardwraps = true; + options.inlineLinks = true; + options.autoLinks = true; + options.reverseHtmlSmartPunctuation = true; + remark = new Remark(options); + //Stop remark from stripping file protocol in an href + try { + Field cleanerField = Remark.class.getDeclaredField("cleaner"); + cleanerField.setAccessible(true); + + Cleaner c = (Cleaner) cleanerField.get(remark); + + Field whitelistField = Cleaner.class.getDeclaredField("whitelist"); + whitelistField.setAccessible(true); + + Whitelist w = (Whitelist) whitelistField.get(c); + + w.addProtocols("a", "href", "file"); + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { + LOGGER.severe("Unable to modify jsoup to include file protocols "+ e.getMessage()); + } + } + + public static String convert(String html) { + if(!htmlPattern.matcher(html).matches()) { + return StringEscapeUtils.unescapeXml(html); // is not html so it can be returned + } + return remark.convert(html); + } + +} diff --git a/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/utils/MarkdownConverterTest.java b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/utils/MarkdownConverterTest.java new file mode 100644 index 0000000000..4705b473e2 --- /dev/null +++ b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/utils/MarkdownConverterTest.java @@ -0,0 +1,51 @@ +/******************************************************************************* +* Copyright (c) 2019 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ + +package org.eclipse.lsp4xml.utils; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * MarkdownConverterTest + */ +public class MarkdownConverterTest { + + @Test + public void testHTMLConversion() { + assertEquals("This is `my code`", MarkdownConverter.convert("This is my code")); + assertEquals("This is\n**bold**", MarkdownConverter.convert("This is
bold")); + assertEquals("The `` element is the root of the descriptor.", MarkdownConverter.convert("The <project> element is the root of the descriptor.")); + assertEquals("# Hey Man #", MarkdownConverter.convert("

Hey Man

")); + assertEquals("[Placeholder](https://www.xml.com)", MarkdownConverter.convert("Placeholder")); + + String htmlList = + "
    \n" + + "
  • Coffee
  • \n" + + "
  • Tea
  • \n" + + "
  • Milk
  • \n" + + "
"; + String expectedList = + " * Coffee\n" + + " * Tea\n" + + " * Milk"; + assertEquals(expectedList, MarkdownConverter.convert(htmlList)); + assertEquals("ONLY\\_THIS\\_TEXT", MarkdownConverter.convert("

ONLY_THIS_TEXT

")); + } + + @Test + public void testMarkdownConversion() { + assertEquals("This is `my code`", MarkdownConverter.convert("This is `my code`")); + assertEquals("The `` element is the root of the descriptor.", MarkdownConverter.convert("The `` element is the root of the descriptor.")); + assertEquals("The `` element is the root of the descriptor.", MarkdownConverter.convert("The `<project>` element is the root of the descriptor.")); + } + +} \ No newline at end of file