diff --git a/ide/csl.api/test/unit/src/org/netbeans/modules/csl/api/test/CslTestBase.java b/ide/csl.api/test/unit/src/org/netbeans/modules/csl/api/test/CslTestBase.java index 72c0ba02ae42..4fdec0cea511 100644 --- a/ide/csl.api/test/unit/src/org/netbeans/modules/csl/api/test/CslTestBase.java +++ b/ide/csl.api/test/unit/src/org/netbeans/modules/csl/api/test/CslTestBase.java @@ -4813,4 +4813,4 @@ public CaretLineOffset(int offset, String caretLine) { } } -} +} \ No newline at end of file diff --git a/ide/html.editor/src/org/netbeans/modules/html/editor/hints/Bundle.properties b/ide/html.editor/src/org/netbeans/modules/html/editor/hints/Bundle.properties index ea391606a5dc..1a4e9e1d2d9a 100644 --- a/ide/html.editor/src/org/netbeans/modules/html/editor/hints/Bundle.properties +++ b/ide/html.editor/src/org/netbeans/modules/html/editor/hints/Bundle.properties @@ -106,4 +106,7 @@ MSG_SET_DEFAULT_HTML_VERSION=Change project default to {0} MSG_FatalHtmlRuleName=Fatal Errors MSG_FatalHtmlErrorAddendum=Further processing of the file may be significantly affected by this fatal error. Please fix the problem before continuing editing of this file! +AddMissingAltAttributeRule=Provide Text Alternatives +AddMissingAltAttributeRule_Desc=Find img/applet/area elements where no alt attribute is provided. + #MSG_HINT_GENERATE_REQUIRED_ATTRIBUTES=Generate required attributes \ No newline at end of file diff --git a/ide/html.editor/src/org/netbeans/modules/html/editor/hints/HtmlHintsProvider.java b/ide/html.editor/src/org/netbeans/modules/html/editor/hints/HtmlHintsProvider.java index 4eb17a75f55e..6edd51d9f42d 100644 --- a/ide/html.editor/src/org/netbeans/modules/html/editor/hints/HtmlHintsProvider.java +++ b/ide/html.editor/src/org/netbeans/modules/html/editor/hints/HtmlHintsProvider.java @@ -67,12 +67,16 @@ public class HtmlHintsProvider implements HintsProvider { private static RequestProcessor RP = new RequestProcessor(HtmlHintsProvider.class); private static final Logger LOG = Logger.getLogger(HtmlHintsProvider.class.getName()); + + private volatile boolean cancel = false; + /** * Compute hints applicable to the given compilation info and add to the * given result list. */ @Override public void computeHints(HintsManager manager, RuleContext context, List hints) { + resume(); HtmlParserResult result = (HtmlParserResult) context.parserResult; HtmlVersion version = result.getDetectedHtmlVersion(); FileObject file = result.getSnapshot().getSource().getFileObject(); @@ -80,6 +84,11 @@ public void computeHints(HintsManager manager, RuleContext context, List h //the Hint doesn't allow the fileObject argument to be null return; } + + if (cancel) { + return; + } + Project project = FileOwnerQuery.getOwner(file); boolean xhtml = result.getSyntaxAnalyzerResult().mayBeXhtml(); if (version == null) { @@ -89,32 +98,35 @@ public void computeHints(HintsManager manager, RuleContext context, List h //we cannot set the default anywhere, just show a warning message hints.add(new Hint(getRule(Severity.INFO), - NbBundle.getMessage(HtmlHintsProvider.class, "MSG_CANNOT_DETERMINE_HTML_VERSION_NO_PROJECT"), - file, - new OffsetRange(0, 0), - Collections.emptyList(), - 100) { + NbBundle.getMessage(HtmlHintsProvider.class, "MSG_CANNOT_DETERMINE_HTML_VERSION_NO_PROJECT"), + file, + new OffsetRange(0, 0), + Collections.emptyList(), + 100) { }); } else { + if (cancel) { + return; + } + //no doctype declaration found, generate the set default project html version hint HtmlVersion defaulted = ProjectDefaultHtmlSourceVersionController.getDefaultHtmlVersion(project, xhtml); String msg = defaulted == null - ? NbBundle.getMessage(HtmlHintsProvider.class, xhtml ? "MSG_CANNOT_DETERMINE_XHTML_VERSION" : "MSG_CANNOT_DETERMINE_HTML_VERSION") - : NbBundle.getMessage(HtmlHintsProvider.class, xhtml ? "MSG_CANNOT_DETERMINE_XHTML_VERSION_DEFAULTED_ALREADY" : "MSG_CANNOT_DETERMINE_HTML_VERSION_DEFAULTED_ALREADY", defaulted.getDisplayName()); + ? NbBundle.getMessage(HtmlHintsProvider.class, xhtml ? "MSG_CANNOT_DETERMINE_XHTML_VERSION" : "MSG_CANNOT_DETERMINE_HTML_VERSION") + : NbBundle.getMessage(HtmlHintsProvider.class, xhtml ? "MSG_CANNOT_DETERMINE_XHTML_VERSION_DEFAULTED_ALREADY" : "MSG_CANNOT_DETERMINE_HTML_VERSION_DEFAULTED_ALREADY", defaulted.getDisplayName()); hints.add(new Hint(getRule(Severity.INFO), - msg, - file, - new OffsetRange(0, 0), - generateSetDefaultHtmlVersionHints(project, result.getSnapshot().getSource().getDocument(false), xhtml), - 100) { + msg, + file, + new OffsetRange(0, 0), + generateSetDefaultHtmlVersionHints(project, result.getSnapshot().getSource().getDocument(false), xhtml), + 100) { }); } } //add html-css related hints // HtmlCssHints.computeHints(manager, context, hints); - } private static List generateSetDefaultHtmlVersionHints(Project project, Document doc, boolean xhtml) { @@ -136,46 +148,69 @@ private static List generateSetDefaultHtmlVersionHints(Project project, */ @Override public void computeSuggestions(HintsManager manager, RuleContext context, List suggestions, int caretOffset) { + resume(); int errorType = 0; if (context instanceof HtmlErrorFilterContext) { errorType = ((HtmlErrorFilterContext) context).isOnlyBadging() ? 2 : 1; } HtmlParserResult result = (HtmlParserResult) context.parserResult; SyntaxAnalyzerResult saresult = result.getSyntaxAnalyzerResult(); - + + if (cancel) { + return; + } + if (isErrorCheckingEnabled(result)) { - HtmlRuleContext htmlRuleContext = new HtmlRuleContext(result, saresult, Collections.emptyList()); + HtmlRuleContext htmlRuleContext = initializeContext(context, saresult, Collections.emptyList()); + if (cancel) { + return; + } for (org.netbeans.modules.html.editor.hints.HtmlRule rule : getSortedRules(manager, context, true)) { //line hints + if (cancel) { + return; + } + //skip the rule if we are called from the tasklist, //the rule is not supposed to show in tasklist and is not badging - if(errorType > 0 && !rule.showInTasklist() && !(rule instanceof ErrorBadgingRule)) { + if (errorType > 0 && !rule.showInTasklist() && !(rule instanceof ErrorBadgingRule)) { continue; } // do not run regular rules when only error badging, or vice versa if ((errorType == 2) != (rule instanceof ErrorBadgingRule)) { continue; } - if(manager.isEnabled(rule)) { + if (manager.isEnabled(rule)) { rule.run(htmlRuleContext, suggestions); } } } - + + if (cancel) { + return; + } + //TODO remove the hardcoding - put the rules to the layer - if(ExtractInlinedStyleRule.SINGLETON.appliesTo(context)) { + if (ExtractInlinedStyleRule.SINGLETON.appliesTo(context)) { suggestions.add(new ExtractInlinedStyleHint(context, new OffsetRange(context.caretOffset, context.caretOffset))); } - if(RemoveSurroundingTag.RULE.appliesTo(context)) { + if (RemoveSurroundingTag.RULE.appliesTo(context)) { suggestions.add(new RemoveSurroundingTag(context, new OffsetRange(context.caretOffset, context.caretOffset))); } - + + if (cancel) { + return; + } + //html extensions String sourceMimetype = WebPageMetadata.getContentMimeType(result, true); for (HtmlExtension ext : HtmlExtensions.getRegisteredExtensions(sourceMimetype)) { + if (cancel) { + return; + } ext.computeSuggestions(manager, context, suggestions, caretOffset); } - + } /** @@ -184,13 +219,21 @@ public void computeSuggestions(HintsManager manager, RuleContext context, List suggestions, int start, int end) { + resume(); //html extensions - HtmlParserResult result = (HtmlParserResult)context.parserResult; + HtmlParserResult result = (HtmlParserResult) context.parserResult; String sourceMimetype = WebPageMetadata.getContentMimeType(result, true); for (HtmlExtension ext : HtmlExtensions.getRegisteredExtensions(sourceMimetype)) { + if (cancel) { + return; + } ext.computeSelectionHints(manager, context, suggestions, start, end); } + if (cancel) { + return; + } + suggestions.add(new SurroundWithTag(context, new OffsetRange(start, end))); suggestions.add(new RemoveSurroundingTag(context, new OffsetRange(start, end))); } @@ -202,6 +245,7 @@ public void computeSelectionHints(HintsManager manager, RuleContext context, Lis */ @Override public void computeErrors(HintsManager manager, RuleContext context, List hints, List unhandled) { + resume(); int errorType = 0; // in the case the context is a regular one, not for indexing, all enabled hints should run. if (context instanceof HtmlErrorFilterContext) { @@ -209,7 +253,11 @@ public void computeErrors(HintsManager manager, RuleContext context, List } LOG.log(Level.FINE, "computing errors (errorType:{0}) for source {1}", new Object[]{errorType, context.parserResult.getSnapshot().getSource()}); LOG.log(Level.FINER, null, new Exception()); - + + if (cancel) { + return; + } + HtmlParserResult result = (HtmlParserResult) context.parserResult; SyntaxAnalyzerResult saresult = result.getSyntaxAnalyzerResult(); Snapshot snapshot = result.getSnapshot(); @@ -220,6 +268,10 @@ public void computeErrors(HintsManager manager, RuleContext context, List return; } + if (cancel) { + return; + } + //add default fixes List defaultFixes = new ArrayList<>(3); if (!isErrorCheckingDisabledForFile(result)) { @@ -229,7 +281,11 @@ public void computeErrors(HintsManager manager, RuleContext context, List defaultFixes.add(new DisableErrorChecksForMimetypeFix(saresult)); } - HtmlRuleContext htmlRuleContext = new HtmlRuleContext(result, saresult, defaultFixes); + HtmlRuleContext htmlRuleContext = initializeContext(context, saresult, defaultFixes); + + if (cancel) { + return; + } //filter out fatal errors and remove them from the html validator hints processing Collection fatalErrors = new ArrayList<>(); @@ -238,18 +294,21 @@ public void computeErrors(HintsManager manager, RuleContext context, List fatalErrors.add(e); } } - //To resolve following + //To resolve following //Bug 200801 - Fatal error hint for mixed php/html code - //but keep the behavior described in - //Bug 199104 - No error for unmatched
tag + //but keep the behavior described in + //Bug 199104 - No error for unmatched
tag //I need to keep the fatal errors enabled only for something which is xml-like // //Really proper solution would be to introduce a facility which would filter - //out the error messages selectively and keep just those whose cannot be + //out the error messages selectively and keep just those whose cannot be //false errors caused by a templating language. The tags pairing in facelets //is nice example as described in the issue above. if (isXmlBasedMimetype(saresult)) { for (Error e : fatalErrors) { + if (cancel) { + return; + } //remove the fatal error from the list of errors for further processing htmlRuleContext.getLeftDiagnostics().remove(e); @@ -260,26 +319,33 @@ public void computeErrors(HintsManager manager, RuleContext context, List String message = new StringBuilder().append(e.getDescription()).append('\n').append(NbBundle.getMessage(HtmlValidatorRule.class, "MSG_FatalHtmlErrorAddendum")).toString(); //add a special hint for the fatal error - // TODO - should FatalHtmlRule implement ErrorBadginRule + // TODO - should FatalHtmlRule implement ErrorBadginRule Hint fatalErrorHint = new Hint(new FatalHtmlRule(), - message, - fo, - EmbeddingUtil.getErrorOffsetRange(e, snapshot), - Collections.emptyList(), - 5);//looks like lower number o the priority means higher priority + message, + fo, + EmbeddingUtil.getErrorOffsetRange(e, snapshot), + Collections.emptyList(), + 5);//looks like lower number o the priority means higher priority hints.add(fatalErrorHint); } } + if (cancel) { + return; + } + //now process the non-fatal errors if (isErrorCheckingEnabled(result)) { for (org.netbeans.modules.html.editor.hints.HtmlRule rule : getSortedRules(manager, context, false)) { + if (cancel) { + return; + } LOG.log(Level.FINE, "checking rule {0}", rule.getDisplayName()); //skip the rule if we are called from the tasklist, //the rule is not supposed to show in tasklist and is not badging - if(errorType > 0 && !rule.showInTasklist() && !(rule instanceof ErrorBadgingRule)) { + if (errorType > 0 && !rule.showInTasklist() && !(rule instanceof ErrorBadgingRule)) { continue; } // do not run regular rules when only error badging, or vice versa @@ -287,7 +353,7 @@ public void computeErrors(HintsManager manager, RuleContext context, List continue; } boolean enabled = manager.isEnabled(rule); - //html validator error categories workaround. + //html validator error categories workaround. //See the HtmlValidatorRule.isSpecialHtmlValidatorRule() documentation LOG.log(Level.FINE, "rule runs"); if (rule.isSpecialHtmlValidatorRule()) { @@ -310,18 +376,25 @@ public void computeErrors(HintsManager manager, RuleContext context, List } Hint h = new Hint(new HtmlRule(HintSeverity.INFO, false), - NbBundle.getMessage(HtmlHintsProvider.class, "MSG_HINT_ENABLE_ERROR_CHECKS_FILE_DESCR"), //NOI18N - fo, - new OffsetRange(0, 0), - fixes, - 50); + NbBundle.getMessage(HtmlHintsProvider.class, "MSG_HINT_ENABLE_ERROR_CHECKS_FILE_DESCR"), //NOI18N + fo, + new OffsetRange(0, 0), + fixes, + 50); hints.add(h); } + if (cancel) { + return; + } + //html extensions String sourceMimetype = WebPageMetadata.getContentMimeType(result, true); for (HtmlExtension ext : HtmlExtensions.getRegisteredExtensions(sourceMimetype)) { + if (cancel) { + return; + } ext.computeErrors(manager, context, hints, unhandled); } @@ -330,7 +403,7 @@ public void computeErrors(HintsManager manager, RuleContext context, List /* test */ static List getSortedRules(HintsManager manager, RuleContext context, boolean lineContext) { Map> allHints = manager.getHints(lineContext, context); List ids - = (List) allHints.get(org.netbeans.modules.html.editor.hints.HtmlRule.Kinds.DEFAULT); + = (List) allHints.get(org.netbeans.modules.html.editor.hints.HtmlRule.Kinds.DEFAULT); if (ids == null) { return Collections.emptyList(); } @@ -408,21 +481,46 @@ public void computeErrors(HintsManager manager, RuleContext context, List */ @Override public void cancel() { + cancel = true; + } + + private void resume() { + cancel = false; + } + + private HtmlRuleContext initializeContext(RuleContext context, SyntaxAnalyzerResult saresult, List defaultFixes) { + HtmlRuleContext htmlRuleContext; + if (context instanceof HtmlRuleContext) { + htmlRuleContext = (HtmlRuleContext) context; + } else { + // Fallback: create a new HtmlRuleContext and copy base fields + htmlRuleContext = new HtmlRuleContext(); + htmlRuleContext.parserResult = context.parserResult; + htmlRuleContext.doc = context.doc; + htmlRuleContext.caretOffset = context.caretOffset; + htmlRuleContext.selectionStart = context.selectionStart; + htmlRuleContext.selectionEnd = context.selectionEnd; + htmlRuleContext.manager = context.manager; + } + htmlRuleContext.initialize(saresult, defaultFixes); + return htmlRuleContext; } /** - *

Optional builtin Rules. Typically you don't use this; you register - * your rules in your filesystem layer in the gsf-hints/mimetype1/mimetype2 + *

+ * Optional builtin Rules. Typically you don't use this; you register your + * rules in your filesystem layer in the gsf-hints/mimetype1/mimetype2 * folder, for example gsf-hints/text/x-ruby/. Error hints should go in the * "errors" folder, selection hints should go in the "selection" folder, and * all other hints should go in the "hints" folder (but note that you can * create localized folders and organize them under hints; these categories * are shown in the hints options panel. Hints returned from this method - * will be placed in the "general" folder.

This method is primarily - * intended for rules that should be added dynamically, for example for - * Rules that have a many different flavors yet a single implementation - * class (such as JavaScript's StrictWarning rule which wraps a number of - * builtin parser warnings.) + * will be placed in the "general" folder.

+ *

+ * This method is primarily intended for rules that should be added + * dynamically, for example for Rules that have a many different flavors yet + * a single implementation class (such as JavaScript's StrictWarning rule + * which wraps a number of builtin parser warnings.) * * @return A list of rules that are builtin, or null or an empty list when * there are no builtins @@ -437,11 +535,11 @@ public List getBuiltinRules() { * implementations of this interface created subclasses of the RuleContext * that can be passed around to all the executed rules. * - * @return A new instance of a RuleContext object + * @return A new instance of a HtmlRuleContext object */ @Override public RuleContext createRuleContext() { - return new RuleContext(); + return new HtmlRuleContext(); } private static final HtmlRule ERROR_RULE = new HtmlRule(HintSeverity.ERROR, true); private static final HtmlRule WARNING_RULE = new HtmlRule(HintSeverity.WARNING, true); @@ -567,7 +665,7 @@ public void implement() throws Exception { //refresh Action Items for this file reindexFile(fo); - + refreshDocument(fo); } @@ -588,7 +686,7 @@ private static void reindexFile(final FileObject fo) { public void run() { //refresh Action Items for this file IndexingManager.getDefault().refreshIndexAndWait(fo.getParent().toURL(), - Collections.singleton(fo.toURL()), true, false); + Collections.singleton(fo.toURL()), true, false); } }); } @@ -613,7 +711,7 @@ public void run() { EditorCookie editorCookie = dobj.getLookup().lookup(EditorCookie.class); StyledDocument document = editorCookie.openDocument(); forceReparse(document); - } catch (IOException ex) { + } catch (IOException ex) { Exceptions.printStackTrace(ex); } } diff --git a/ide/html.editor/src/org/netbeans/modules/html/editor/hints/HtmlRuleContext.java b/ide/html.editor/src/org/netbeans/modules/html/editor/hints/HtmlRuleContext.java index d518ac726c54..d36b55ca0ef5 100644 --- a/ide/html.editor/src/org/netbeans/modules/html/editor/hints/HtmlRuleContext.java +++ b/ide/html.editor/src/org/netbeans/modules/html/editor/hints/HtmlRuleContext.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.List; @@ -29,6 +30,7 @@ import org.netbeans.api.project.Project; import org.netbeans.modules.csl.api.Error; import org.netbeans.modules.csl.api.HintFix; +import org.netbeans.modules.csl.api.RuleContext; import org.netbeans.modules.csl.api.Severity; import org.netbeans.modules.css.indexing.api.CssIndex; import org.netbeans.modules.html.editor.api.gsf.HtmlParserResult; @@ -43,44 +45,56 @@ * * @author marekfukala */ -public class HtmlRuleContext { +public class HtmlRuleContext extends RuleContext { - private final SyntaxAnalyzerResult syntaxAnalyzerResult; - private final HtmlParserResult parserResult; - private final List defaultFixes; - private final List leftDiagnostics; + private SyntaxAnalyzerResult syntaxAnalyzerResult; + private List defaultFixes; + private List leftDiagnostics; private CssIndex cssIndex; private DependenciesGraph cssDependencies; - private final Lines lines; - private final Collection linesWithHints; + private Lines lines; + private Collection linesWithHints; - public HtmlRuleContext(HtmlParserResult parserResult, SyntaxAnalyzerResult syntaxAnalyzerResult, List defaultFixes) { - this.parserResult = parserResult; + public void initialize(SyntaxAnalyzerResult syntaxAnalyzerResult, List defaultFixes) { this.syntaxAnalyzerResult = syntaxAnalyzerResult; this.defaultFixes = defaultFixes; - this.leftDiagnostics = new ArrayList<>(parserResult.getDiagnostics(EnumSet.allOf(Severity.class))); - this.lines = new Lines(parserResult.getSnapshot().getText()); + + HtmlParserResult htmlResult = getHtmlParserResult(); + if (htmlResult != null) { + this.leftDiagnostics = new ArrayList<>(htmlResult.getDiagnostics(EnumSet.allOf(Severity.class))); + this.lines = new Lines(htmlResult.getSnapshot().getText()); + } else { + this.leftDiagnostics = Collections.emptyList(); + this.lines = null; + } + this.linesWithHints = new HashSet<>(); } public boolean isFirstHintForPosition(int offset) { + if (lines == null) { + return true; + } + try { int lineIndex = lines.getLineIndex(offset); - if(linesWithHints.contains(lineIndex)) { + if (linesWithHints.contains(lineIndex)) { return false; } else { linesWithHints.add(lineIndex); return true; } - } catch (BadLocationException ex) { Exceptions.printStackTrace(ex); } return true; } - + public HtmlParserResult getHtmlParserResult() { - return parserResult; + if (parserResult instanceof HtmlParserResult) { + return (HtmlParserResult) parserResult; + } + return null; } public SyntaxAnalyzerResult getSyntaxAnalyzerResult() { @@ -112,15 +126,15 @@ public synchronized CssIndex getCssIndex() throws IOException { } return cssIndex; } - + public synchronized DependenciesGraph getCssDependenciesGraph() throws IOException { - if(cssDependencies == null) { + if (cssDependencies == null) { CssIndex index = getCssIndex(); - if(index != null) { + if (index != null) { cssDependencies = index.getDependencies(getFile()); } } return cssDependencies; } - + } diff --git a/ide/html.editor/src/org/netbeans/modules/html/editor/hints/other/AddMissingAltAttributeHint.java b/ide/html.editor/src/org/netbeans/modules/html/editor/hints/other/AddMissingAltAttributeHint.java new file mode 100644 index 000000000000..2657c66a3460 --- /dev/null +++ b/ide/html.editor/src/org/netbeans/modules/html/editor/hints/other/AddMissingAltAttributeHint.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.netbeans.modules.html.editor.hints.other; + +import java.util.Collections; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.swing.text.BadLocationException; +import org.netbeans.editor.BaseDocument; +import org.netbeans.modules.csl.api.Hint; +import org.netbeans.modules.csl.api.HintFix; +import org.netbeans.modules.csl.api.OffsetRange; +import org.netbeans.modules.html.editor.hints.HtmlRuleContext; +import org.netbeans.modules.html.editor.utils.HtmlTagContextUtils; + +/** + * + * @author Christian Lenz + */ +public class AddMissingAltAttributeHint extends Hint { + + public AddMissingAltAttributeHint(HtmlRuleContext context, OffsetRange range) { + super(AddMissingAltAttributeRule.getInstance(), + AddMissingAltAttributeRule.getInstance().getDescription(), + context.getFile(), + range, + Collections.singletonList(new AddMissingAltAttributeHintFix(context, range)), + 10); + } + + private static class AddMissingAltAttributeHintFix implements HintFix { + + private static final Logger LOGGER = Logger.getLogger(AddMissingAltAttributeHintFix.class.getSimpleName()); + + private final HtmlRuleContext context; + private final OffsetRange range; + + public AddMissingAltAttributeHintFix(HtmlRuleContext context, OffsetRange range) { + this.context = context; + this.range = range; + } + + @Override + public String getDescription() { + return AddMissingAltAttributeRule.getInstance().getDisplayName(); + } + + @Override + public void implement() throws Exception { + BaseDocument document = (BaseDocument) context.getSnapshot().getSource().getDocument(true); + document.runAtomic(() -> { + try { + OffsetRange adjustedRange = HtmlTagContextUtils.adjustContextRange(document, range.getStart(), range.getEnd(), true); + String tagContent = document.getText(adjustedRange.getStart(), adjustedRange.getLength()); + + // Find last self-closing or non self-closing tag + Pattern closingTagPattern = Pattern.compile("(/?>)"); + Matcher closingTagMatcher = closingTagPattern.matcher(tagContent); + + if (closingTagMatcher.find()) { + int altInsertPosition = adjustedRange.getStart() + closingTagMatcher.start(1); + + // Check whether a space before alt is needed or not + boolean needsSpaceBefore = altInsertPosition == 0 || tagContent.charAt(closingTagMatcher.start(1) - 1) != ' '; + boolean isSelfClosing = closingTagMatcher.group(0).endsWith("/>"); + String altAttribute = (needsSpaceBefore ? " " : "") + "alt=\"\"" + (isSelfClosing ? " " : ""); // NOI18N + + document.insertString(altInsertPosition, altAttribute, null); + } + } catch (BadLocationException ex) { + LOGGER.log(Level.WARNING, "Invalid offset: {0}", ex.offsetRequested()); + } + }); + } + + @Override + public boolean isSafe() { + return true; + } + + @Override + public boolean isInteractive() { + return false; + } + } +} diff --git a/ide/html.editor/src/org/netbeans/modules/html/editor/hints/other/AddMissingAltAttributeRule.java b/ide/html.editor/src/org/netbeans/modules/html/editor/hints/other/AddMissingAltAttributeRule.java new file mode 100644 index 000000000000..40ea51b6ab77 --- /dev/null +++ b/ide/html.editor/src/org/netbeans/modules/html/editor/hints/other/AddMissingAltAttributeRule.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.netbeans.modules.html.editor.hints.other; + +import java.io.IOException; +import java.util.List; +import org.netbeans.modules.csl.api.Hint; +import org.netbeans.modules.csl.api.HintSeverity; +import org.netbeans.modules.csl.api.RuleContext; +import org.netbeans.modules.html.editor.api.gsf.HtmlParserResult; +import org.netbeans.modules.html.editor.hints.HtmlRule; +import org.netbeans.modules.html.editor.hints.HtmlRuleContext; +import org.netbeans.modules.html.editor.lib.api.elements.ElementType; +import org.netbeans.modules.html.editor.lib.api.elements.ElementUtils; +import org.openide.util.Exceptions; + +/** + * + * @author Christian Lenz + */ +public class AddMissingAltAttributeRule extends HtmlRule { + + private final static AddMissingAltAttributeRule INSTANCE = new AddMissingAltAttributeRule(); + + private AddMissingAltAttributeRule() { + } + + public static AddMissingAltAttributeRule getInstance() { + return INSTANCE; + } + + @Override + public boolean appliesTo(RuleContext context) { + return true; + } + + @Override + public boolean showInTasklist() { + return true; + } + + @Override + public HintSeverity getDefaultSeverity() { + return HintSeverity.CURRENT_LINE_WARNING; + } + + @Override + protected void run(HtmlRuleContext context, List result) { + try { + HtmlParserResult parserResult = context.getHtmlParserResult(); + AltAttributeVisitor visitor = new AltAttributeVisitor(this, context, result); + + ElementUtils.visitChildren(parserResult.root(), visitor, ElementType.OPEN_TAG); + } catch (IOException ioe) { + Exceptions.printStackTrace(ioe); + } + } +} diff --git a/ide/html.editor/src/org/netbeans/modules/html/editor/hints/other/AltAttributeVisitor.java b/ide/html.editor/src/org/netbeans/modules/html/editor/hints/other/AltAttributeVisitor.java new file mode 100644 index 000000000000..7abd532fcf4b --- /dev/null +++ b/ide/html.editor/src/org/netbeans/modules/html/editor/hints/other/AltAttributeVisitor.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.netbeans.modules.html.editor.hints.other; + +import java.io.IOException; +import java.util.*; +import org.netbeans.modules.csl.api.Hint; +import org.netbeans.modules.csl.api.OffsetRange; +import org.netbeans.modules.csl.api.Rule; +import org.netbeans.modules.html.editor.hints.HtmlRuleContext; +import org.netbeans.modules.html.editor.lib.api.elements.*; + +/** + * + * @author Christian Lenz + */ +public class AltAttributeVisitor implements ElementVisitor { + + private static final String ALT_ATTR = "alt"; // NOI18N + + private final HtmlRuleContext context; + private final List hints; + + public AltAttributeVisitor(Rule rule, HtmlRuleContext context, List hints) throws IOException { + this.context = context; + this.hints = hints; + } + + @Override + public void visit(Element node) { + // We should only be invoked for opening tags + if (!(node instanceof OpenTag)) { + return; + } + + // We are only interested in img, area, applet elements + String lowerCaseTag = node.id().toString().toLowerCase(); + if (!(lowerCaseTag.equals("img") || lowerCaseTag.equals("area") || lowerCaseTag.equals("applet"))) { // NOI18N + return; + } + + OpenTag ot = (OpenTag) node; + if (ot.getAttribute(ALT_ATTR) == null) { + hints.add(new AddMissingAltAttributeHint(context, new OffsetRange(node.from(), node.to()))); + } + } +} diff --git a/ide/html.editor/src/org/netbeans/modules/html/editor/resources/layer.xml b/ide/html.editor/src/org/netbeans/modules/html/editor/resources/layer.xml index 3895708ee15e..c4c33ded68f0 100644 --- a/ide/html.editor/src/org/netbeans/modules/html/editor/resources/layer.xml +++ b/ide/html.editor/src/org/netbeans/modules/html/editor/resources/layer.xml @@ -534,6 +534,9 @@ + + + diff --git a/ide/html.editor/src/org/netbeans/modules/html/editor/utils/HtmlTagContextUtils.java b/ide/html.editor/src/org/netbeans/modules/html/editor/utils/HtmlTagContextUtils.java new file mode 100644 index 000000000000..a5dcd63ef2a4 --- /dev/null +++ b/ide/html.editor/src/org/netbeans/modules/html/editor/utils/HtmlTagContextUtils.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.netbeans.modules.html.editor.utils; + +import java.util.concurrent.atomic.AtomicReference; +import javax.swing.text.Document; +import org.netbeans.api.html.lexer.HTMLTokenId; +import org.netbeans.api.lexer.Token; +import org.netbeans.api.lexer.TokenSequence; +import org.netbeans.modules.csl.api.OffsetRange; +import org.netbeans.modules.html.editor.api.Utils; + +/** + * + * @author Christian Lenz + */ +public final class HtmlTagContextUtils { + + private HtmlTagContextUtils() { + } + + public static OffsetRange adjustContextRange(final Document doc, final int from, final int to, final boolean beforeClosingToken) { + final AtomicReference ret = new AtomicReference<>(); + ret.set(new OffsetRange(from, to)); //return the same pair by default + doc.render(() -> { + TokenSequence ts = Utils.getJoinedHtmlSequence(doc, from); + + if (ts == null) { + //no html token sequence at the offset, try to + //TODO possibly try to travese the top level sequence backward + //and try to find an html embedding. + return; + } + + Token openTag = Utils.findTagOpenToken(ts); + + if (openTag == null) { + return; + } + + int adjustedFrom = ts.offset(); + + //now try to find the tag's end + ts.move(to); + int adjustedTo = -1; + while (ts.moveNext()) { + Token t = ts.token(); + + if (t == null) { + return; + } + + if (t.id() == HTMLTokenId.TAG_CLOSE_SYMBOL) { + adjustedTo = beforeClosingToken ? ts.offset() : ts.offset() + t.length(); + + break; + } else if (t.id() == HTMLTokenId.TEXT) { + //do not go too far - out of the tag + break; + } + } + + if (adjustedTo == -1) { + return; + } + + //we found the adjusted range + ret.set(new OffsetRange(adjustedFrom, adjustedTo)); + }); + return ret.get(); + } +} diff --git a/ide/html.editor/test/unit/data/testfiles/hints/addMissingAltAttribute1.html b/ide/html.editor/test/unit/data/testfiles/hints/addMissingAltAttribute1.html new file mode 100644 index 000000000000..9f8d465e76ae --- /dev/null +++ b/ide/html.editor/test/unit/data/testfiles/hints/addMissingAltAttribute1.html @@ -0,0 +1,32 @@ + + + + + TODO supply a title + + + + +

TODO write content
+ + + diff --git a/ide/html.editor/test/unit/data/testfiles/hints/addMissingAltAttribute1.html.testProvideTextAlternativeHint.hints b/ide/html.editor/test/unit/data/testfiles/hints/addMissingAltAttribute1.html.testProvideTextAlternativeHint.hints new file mode 100644 index 000000000000..0ab0c5fd1cf4 --- /dev/null +++ b/ide/html.editor/test/unit/data/testfiles/hints/addMissingAltAttribute1.html.testProvideTextAlternativeHint.hints @@ -0,0 +1,4 @@ + + ------- +HINT:Find img/applet/area elements where no alt attribute is provided. +FIX:Provide Text Alternatives diff --git a/ide/html.editor/test/unit/data/testfiles/hints/addMissingAltAttribute1.html.testProvideTextAlternativeHintFix_01.fixed b/ide/html.editor/test/unit/data/testfiles/hints/addMissingAltAttribute1.html.testProvideTextAlternativeHintFix_01.fixed new file mode 100644 index 000000000000..58ebb3c874f0 --- /dev/null +++ b/ide/html.editor/test/unit/data/testfiles/hints/addMissingAltAttribute1.html.testProvideTextAlternativeHintFix_01.fixed @@ -0,0 +1,32 @@ + + + + + TODO supply a title + + + + +
TODO write content
+ + + diff --git a/ide/html.editor/test/unit/src/org/netbeans/modules/html/editor/hints/other/AddMissingAltAttributeRuleTest.java b/ide/html.editor/test/unit/src/org/netbeans/modules/html/editor/hints/other/AddMissingAltAttributeRuleTest.java new file mode 100644 index 000000000000..e90c89163473 --- /dev/null +++ b/ide/html.editor/test/unit/src/org/netbeans/modules/html/editor/hints/other/AddMissingAltAttributeRuleTest.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.netbeans.modules.html.editor.hints.other; + +import org.netbeans.modules.csl.api.Rule; +import org.netbeans.modules.html.editor.test.TestBase; + +/** + * + * @author Christian Lenz + */ +public class AddMissingAltAttributeRuleTest extends TestBase { + + public AddMissingAltAttributeRuleTest(String testName) { + super(testName); + } + + private Rule createRule() { + return AddMissingAltAttributeRule.getInstance(); + } + + public void testProvideTextAlternativeHint() throws Exception { + checkHints(this, createRule(), "testfiles/hints/addMissingAltAttribute1.html", "