| /*=============================================================================# |
| # Copyright (c) 2015, 2021 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.internal.redocs.wikitext.r.ui.processing; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.regex.Matcher; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.SubMonitor; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.osgi.util.NLS; |
| |
| import org.eclipse.statet.jcommons.text.core.BasicTextRegion; |
| import org.eclipse.statet.jcommons.text.core.TextLineInformation; |
| import org.eclipse.statet.jcommons.text.core.TextRegion; |
| |
| import org.eclipse.statet.docmlet.base.ui.DocmlBaseUI; |
| import org.eclipse.statet.docmlet.base.ui.processing.DocProcessingConfig; |
| import org.eclipse.statet.docmlet.wikitext.core.source.extdoc.YamlBlockWeaveParticipant; |
| import org.eclipse.statet.ltk.core.Ltk; |
| import org.eclipse.statet.ltk.core.SourceContent; |
| import org.eclipse.statet.ltk.model.core.LtkModels; |
| import org.eclipse.statet.ltk.model.core.element.SourceUnit; |
| import org.eclipse.statet.redocs.wikitext.r.core.WikitextRweaveCore; |
| import org.eclipse.statet.yaml.core.ast.Collection; |
| import org.eclipse.statet.yaml.core.ast.NodeType; |
| import org.eclipse.statet.yaml.core.ast.SourceComponent; |
| import org.eclipse.statet.yaml.core.ast.Tuple; |
| import org.eclipse.statet.yaml.core.ast.YamlAstNode; |
| import org.eclipse.statet.yaml.core.ast.YamlAstVisitor; |
| import org.eclipse.statet.yaml.core.ast.YamlParser; |
| |
| |
| public class YamlFormatDetector { |
| |
| |
| private static final String FAIL= "<fail>"; //$NON-NLS-1$ |
| |
| |
| private final String modelTypeId; |
| |
| private Matcher extValidator; |
| |
| |
| public YamlFormatDetector(final String modelTypeId) { |
| this.modelTypeId= modelTypeId; |
| } |
| |
| |
| public String detect(final IFile file, final SubMonitor m) throws CoreException { |
| m.beginTask(NLS.bind("Detecting output format of ''{0}''...", file.getName()), |
| 10 ); |
| |
| final SourceUnit unit= LtkModels.getSourceUnitManager().getSourceUnit(this.modelTypeId, |
| Ltk.PERSISTENCE_CONTEXT, file, true, m.newChild(1)); |
| try { |
| final SourceContent content= unit.getContent(m.newChild(1)); |
| m.worked(0); |
| final TextRegion yamlRegion= getYamlBlockRegion(content); |
| m.worked(1); |
| |
| final YamlParser yamlParser= new YamlParser(); |
| yamlParser.setScalarText(true); |
| |
| final String code= content.getString(yamlRegion); |
| final SourceComponent block= yamlParser.parse(code, null, yamlRegion.getStartOffset()); |
| |
| final String format= searchOutputInfo(block, code); |
| final String ext= toExtension(format); |
| |
| if (!isValidExt(ext)) { |
| throw new CoreException(new Status(IStatus.ERROR, DocmlBaseUI.BUNDLE_ID, |
| NLS.bind("Failed to detect file extension for format ''{0}''.", |
| format ))); |
| } |
| |
| return ext; |
| } |
| catch (final Exception e) { |
| throw new CoreException(new Status(IStatus.ERROR, WikitextRweaveCore.BUNDLE_ID, 0, |
| NLS.bind("Failed to detect output format specified in doc (YAML) for ''{0}''.", |
| file.getName() ), |
| e )); |
| } |
| finally { |
| unit.disconnect(m); |
| } |
| } |
| |
| |
| private TextRegion getYamlBlockRegion(final SourceContent sourceContent) |
| throws BadLocationException, CoreException { |
| final TextLineInformation lines= sourceContent.getStringLines(); |
| final YamlBlockWeaveParticipant part= new YamlBlockWeaveParticipant(); |
| part.reset(sourceContent); |
| |
| int lineEndOffset= lines.getStartOffset(0); |
| final int numLines= lines.getNumberOfLines(); |
| for (int line= 0; line < numLines; line++) { |
| int lineOffset= lineEndOffset; |
| lineEndOffset= lines.getEndOffset(line); |
| if (part.checkStartLine(lineOffset, lineEndOffset)) { |
| while (++line < numLines) { |
| lineOffset= lineEndOffset; |
| lineEndOffset= lines.getEndOffset(line); |
| if (part.checkEndLine(lineOffset, lineEndOffset)) { |
| return new BasicTextRegion( |
| sourceContent.getStartOffset() + part.getStartOffset(), |
| sourceContent.getStartOffset() + lineEndOffset ); |
| } |
| } |
| } |
| } |
| throw new CoreException(new Status(IStatus.ERROR, WikitextRweaveCore.BUNDLE_ID, |
| "No YAML metadata block found." )); |
| } |
| |
| |
| private String searchOutputInfo(final SourceComponent block, final String code) |
| throws CoreException, InvocationTargetException { |
| |
| class Searcher extends YamlAstVisitor { |
| |
| private String format; |
| private String output; |
| private String outputFormat; |
| |
| @Override |
| public void visit(final Collection node) throws InvocationTargetException { |
| if (node.getYamlParent().getNodeType() == NodeType.DOC_CONTENT |
| && node.getNodeType() == NodeType.MAP) { |
| node.acceptInYamlChildren(this); |
| } |
| } |
| |
| @Override |
| public void visit(final Tuple node) throws InvocationTargetException { |
| if (node.getKeyNode().getNodeType() == NodeType.SCALAR) { |
| final String key= node.getKeyNode().getText(); |
| if (key != null) { |
| if (key.equals("format")) { //$NON-NLS-1$ |
| if (this.format == null) { |
| if (node.getValueNode().getNodeType() == NodeType.SCALAR) { |
| this.format= node.getValueNode().getText(); |
| } |
| if (this.format == null) { |
| this.format= FAIL; |
| } |
| } |
| } |
| else if (key.equals("output")) { //$NON-NLS-1$ |
| if (this.output == null) { |
| switch (node.getValueNode().getNodeType()) { |
| case SCALAR: |
| this.output= node.getValueNode().getText(); |
| break; |
| case MAP: |
| if (node.getValueNode().hasChildren()) { |
| final YamlAstNode firstOutput= node.getValueNode().getChild(0); |
| if (firstOutput.getNodeType() == NodeType.MAP_ENTRY) { |
| checkOutputEntry((Tuple) firstOutput); |
| } |
| } |
| break; |
| default: |
| break; |
| } |
| if (this.output == null) { |
| this.output= FAIL; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private void checkOutputEntry(final Tuple outputEntry) { |
| if (outputEntry.getKeyNode().getNodeType() == NodeType.SCALAR) { |
| this.output= outputEntry.getKeyNode().getText(); |
| } |
| if (outputEntry.getValueNode().getNodeType() == NodeType.MAP) { |
| final Collection outputConfig= (Collection) outputEntry.getValueNode(); |
| for (int i= 0; i < outputConfig.getChildCount(); i++) { |
| final YamlAstNode iChild= outputConfig.getChild(i); |
| if (iChild.getNodeType() == NodeType.MAP_ENTRY) { |
| final Tuple outputConfigEntry= (Tuple) iChild; |
| if (outputConfigEntry.getKeyNode().getNodeType() == NodeType.SCALAR |
| && outputConfigEntry.getKeyNode().getText().equals("format") //$NON-NLS-1$ |
| && outputConfigEntry.getValueNode().getNodeType() == NodeType.SCALAR) { |
| this.outputFormat= outputConfigEntry.getValueNode().getText(); |
| } |
| } |
| } |
| } |
| } |
| |
| } |
| |
| final Searcher searcher= new Searcher(); |
| block.acceptInYaml(searcher); |
| |
| if (searcher.format != null) { |
| if (searcher.format == FAIL) { |
| throw new CoreException(new Status(IStatus.ERROR, DocmlBaseUI.BUNDLE_ID, |
| "Unexpected data for 'format'.\nYAML code:\n" + code )); |
| } |
| else { |
| return searcher.format; |
| } |
| } |
| else if (searcher.output != null) { |
| if (searcher.outputFormat != null && isValidExt(toExtension(searcher.outputFormat))) { |
| return searcher.outputFormat; |
| } |
| else if (searcher.output == FAIL) { |
| throw new CoreException(new Status(IStatus.ERROR, DocmlBaseUI.BUNDLE_ID, |
| "Unexpected data for 'output'.\nYAML code:\n" + code )); |
| } |
| else { |
| return searcher.output; |
| } |
| } |
| else { |
| throw new CoreException(new Status(IStatus.ERROR, DocmlBaseUI.BUNDLE_ID, |
| "No 'format' or 'output' entry found.\nYAML code:\n" + code )); |
| } |
| } |
| |
| |
| private String toExtension(String format) { |
| int idx= format.lastIndexOf(':'); |
| if (idx >= 0) { |
| format= format.substring(idx + 1); |
| } |
| idx= format.indexOf('_'); |
| if (idx >= 0) { |
| format= format.substring(0, idx); |
| } |
| |
| switch (format) { |
| case "native": |
| return "hs"; |
| case "plain": |
| return "txt"; |
| case "markdown": |
| return "md"; |
| case "mediawiki": |
| return "mediawiki"; |
| case "textile": |
| return "textile"; |
| case "asciidoc": |
| return "asciidoc"; |
| case "html": |
| case "html5": |
| return "html"; |
| case "pdf": |
| case "latex": |
| case "beamer": |
| return "pdf"; |
| case "context": |
| return "tex"; |
| case "texinfo": |
| return "texi"; |
| case "docbook": |
| return "dbk"; |
| case "opendocument": |
| case "odt": |
| return "odt"; |
| case "word": |
| case "docx": |
| return "docx"; |
| case "rtf": |
| return "rtf"; |
| case "epub": |
| case "epub3": |
| return "epub"; |
| case "slidy": |
| case "slideous": |
| case "dzslides": |
| case "revealjs": |
| case "s5": |
| return "html"; |
| default: |
| return format; |
| } |
| } |
| |
| private boolean isValidExt(final String ext) { |
| if (this.extValidator == null) { |
| this.extValidator= DocProcessingConfig.VALID_EXT_PATTERN.matcher(ext); |
| } |
| else { |
| this.extValidator.reset(ext); |
| } |
| return this.extValidator.matches(); |
| } |
| |
| } |