")) { // NOI18N
+ pairTag = ""; // NOI18N
+ }
+ if (startsWith(data.first(), "")) { // NOI18N
+ pairTag = "
"; // NOI18N
+ }
+ break;
+ case '{':
+ pairTag = "}"; // NOI18N
+ break;
+ case '$':
+ // no-op
+ break;
+ default:
+ // no-op
+ break;
+ }
+ } else if (pairTag.contentEquals(data.first())) {
+ pairTag = null;
+ }
+ data = wordBroker(currentBlockText, currentOffsetInComment);
+ }
+ currentBlockText = null;
+ }
+ } catch (BadLocationException e) {
+ ErrorManager.getDefault().notify(e);
+ return false;
+ }
+ }
+
+ private int[] findNextPHPDocComment() throws BadLocationException {
+ TokenSequence ts = null;
+ if (doc instanceof AbstractDocument) {
+ AbstractDocument ad = (AbstractDocument) doc;
+ ad.readLock();
+ try {
+ ts = LexUtilities.getPHPTokenSequence(ad, nextBlockStart);
+ } finally {
+ ad.readUnlock();
+ }
+ }
+ if (ts == null) {
+ return new int[]{-1, -1};
+ }
+
+ ts.move(nextBlockStart);
+ while (ts.moveNext()) {
+ if (ts.token().id() == PHPTokenId.PHPDOC_COMMENT) {
+ return new int[]{ts.offset(), ts.offset() + ts.token().length()};
+ }
+ }
+ return new int[]{-1, -1};
+ }
+
+ private void handlePhpdocTag(CharSequence tag) {
+ if ("@see".contentEquals(tag)) { // NOI18N
+ // e.g.
+ // @see MyClass::$items
+ // @see http://example.com/my/bar Documentation of Foo.
+ // ignore next "word"
+ Pair data = wordBroker(currentBlockText, currentOffsetInComment, LetterType.See);
+ currentOffsetInComment = getCurrentOffsetInComment(data);
+ return;
+ }
+
+ if ("@author".contentEquals(tag)) { // NOI18N
+ // ignore everything till the end of the line:
+ Pair data = wordBroker(currentBlockText, currentOffsetInComment);
+ while (data != null) {
+ currentOffsetInComment = getCurrentOffsetInComment(data);
+ if ('\n' == data.first().charAt(0)) {
+ // continue
+ return;
+ }
+ data = wordBroker(currentBlockText, currentOffsetInComment);
+ }
+ return;
+ }
+
+ if (currentDocBlock != null) {
+ List phpDocTags = currentDocBlock.getTags();
+ for (PHPDocTag phpDocTag : phpDocTags) {
+ if (phpDocTag.getStartOffset() == currentOffsetInComment - tag.length()) {
+ if (phpDocTag instanceof PHPDocTypeTag) {
+ handleTypeTag((PHPDocTypeTag) phpDocTag);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ private void handleTypeTag(PHPDocTypeTag docTypeTag) {
+ // ignore types
+ List types = docTypeTag.getTypes();
+ PHPDocTypeNode lastType = null;
+ for (PHPDocTypeNode type : types) {
+ if (lastType == null || lastType.getEndOffset() < type.getEndOffset()) {
+ lastType = type;
+ }
+ }
+ if (lastType != null) {
+ currentOffsetInComment = lastType.getEndOffset();
+ }
+
+ if (docTypeTag instanceof PHPDocMethodTag) {
+ // ignore params
+ PHPDocMethodTag methodTag = (PHPDocMethodTag) docTypeTag;
+ List parameters = methodTag.getParameters();
+ PHPDocVarTypeTag lastParam = null;
+ for (PHPDocVarTypeTag parameter : parameters) {
+ if (lastParam == null || lastParam.getEndOffset() < parameter.getEndOffset()) {
+ lastParam = parameter;
+ }
+ }
+ if (lastParam != null) {
+ currentOffsetInComment = lastParam.getEndOffset();
+ } else {
+ // ignore method name
+ PHPDocNode methodName = methodTag.getMethodName();
+ if (methodName != null) {
+ Pair data = wordBroker(
+ currentBlockText,
+ currentOffsetInComment,
+ LetterType.MethodName
+ );
+ if (data.first().equals(methodName.getValue())) {
+ currentOffsetInComment = getCurrentOffsetInComment(data);
+ }
+ }
+ }
+ }
+ }
+
+ private Pair wordBroker(CharSequence start, int offset) {
+ return wordBroker(start, offset, LetterType.Normal);
+ }
+
+ private Pair wordBroker(CharSequence start, int offset, LetterType letterType) {
+ State state = State.Start;
+ int offsetStart = offset;
+ int currentOffset = offset;
+
+ while (start.length() > currentOffset) {
+ char currentChar = start.charAt(currentOffset);
+ switch (state) {
+ case Start:
+ if (isLetter(currentChar)) {
+ state = State.Letter;
+ offsetStart = currentOffset;
+ break;
+ }
+ if (currentChar == '@' || currentChar == '#') {
+ state = State.Tag;
+ offsetStart = currentOffset;
+ break;
+ }
+ if (currentChar == '<') {
+ state = State.AngleBracket;
+ offsetStart = currentOffset;
+ break;
+ }
+ if (currentChar == '\n' || currentChar == '}') {
+ return Pair.of(start.subSequence(currentOffset, currentOffset + 1), currentOffset);
+ }
+ if (currentChar == '{') {
+ state = State.Brace;
+ offsetStart = currentOffset;
+ break;
+ }
+ if (currentChar == '&') {
+ state = State.Entity;
+ offsetStart = currentOffset;
+ break;
+ }
+ if (currentChar == '$') {
+ state = State.Variable;
+ offsetStart = currentOffset;
+ break;
+ }
+ break;
+ case Letter:
+ if (!isLetter(currentChar)) {
+ if (!letterType.accept(currentChar)) {
+ return Pair.of(start.subSequence(offsetStart, currentOffset), offsetStart);
+ }
+ }
+ break;
+ case Tag:
+ // phpdoc tag e.g. @param, @property-read
+ if (!isLetter(currentChar) && currentChar != '-') {
+ return Pair.of(start.subSequence(offsetStart, currentOffset), offsetStart);
+ }
+ break;
+ case AngleBracket:
+ // tag e.g.
+ if (currentChar == '>') {
+ return Pair.of(start.subSequence(offsetStart, currentOffset + 1), offsetStart);
+ }
+ break;
+ case Brace:
+ if (currentChar == '@') {
+ // inline phpdoc tag e.g. {@inheritdoc}
+ state = State.Tag;
+ break;
+ }
+ currentOffset--;
+ state = State.Start;
+ break;
+ case Entity:
+ // entities e.g. > >
+ if (currentChar == ';') {
+ return Pair.of(start.subSequence(offsetStart, currentOffset + 1), offsetStart);
+ }
+ if (!isLetter(currentChar)
+ && currentChar != '#'
+ && !Character.isDigit(currentChar)) {
+ return Pair.of(start.subSequence(offsetStart, currentOffset), offsetStart);
+ }
+ break;
+ case Variable:
+ // variable e.g. $var_name
+ if (!isLetter(currentChar)
+ && currentChar != '_'
+ && !Character.isDigit(currentChar)) {
+ return Pair.of(start.subSequence(offsetStart, currentOffset), offsetStart);
+ }
+ break;
+ default:
+ assert false;
+ break;
+ }
+ currentOffset++;
+ }
+
+ if (currentOffset > offsetStart) {
+ return Pair.of(start.subSequence(offsetStart, currentOffset), offsetStart);
+ } else {
+ return null;
+ }
+ }
+
+ private static boolean startsWith(CharSequence where, String withWhat) {
+ if (where.length() >= withWhat.length()) {
+ return withWhat.contentEquals(where.subSequence(0, withWhat.length()));
+ }
+ return false;
+ }
+
+ static boolean isIdentifierLike(CharSequence s) {
+ boolean hasCapitalsInside = false;
+ int offset = 1;
+ while (offset < s.length() && !hasCapitalsInside) {
+ hasCapitalsInside |= Character.isUpperCase(s.charAt(offset));
+ offset++;
+ }
+ return hasCapitalsInside;
+ }
+
+ private static int getCurrentOffsetInComment(Pair data) {
+ return data.second() + data.first().length();
+ }
+
+ private static boolean isLetter(char c) {
+ return Character.isLetter(c) || c == '\'';
+ }
+
+ @Override
+ public int getCurrentWordStartOffset() {
+ return currentWordOffset;
+ }
+
+ @Override
+ public CharSequence getCurrentWordText() {
+ return currentWord;
+ }
+
+ @Override
+ public void addChangeListener(ChangeListener listener) {
+ //ignored...
+ }
+
+ @Override
+ public void removeChangeListener(ChangeListener listener) {
+ //ignored...
+ }
+
+}
diff --git a/spellchecker.bindings.php/src/org/netbeans/modules/spellchecker/bindings/php/PHPTokenListProvider.java b/spellchecker.bindings.php/src/org/netbeans/modules/spellchecker/bindings/php/PHPTokenListProvider.java
new file mode 100644
--- /dev/null
+++ b/spellchecker.bindings.php/src/org/netbeans/modules/spellchecker/bindings/php/PHPTokenListProvider.java
@@ -0,0 +1,64 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2016 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
+ * Other names may be trademarks of their respective owners.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common
+ * Development and Distribution License("CDDL") (collectively, the
+ * "License"). You may not use this file except in compliance with the
+ * License. You can obtain a copy of the License at
+ * http://www.netbeans.org/cddl-gplv2.html
+ * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
+ * specific language governing permissions and limitations under the
+ * License. When distributing the software, include this License Header
+ * Notice in each file and include the License file at
+ * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the GPL Version 2 section of the License file that
+ * accompanied this code. If applicable, add the following below the
+ * License Header, with the fields enclosed by brackets [] replaced by
+ * your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * If you wish your version of this file to be governed by only the CDDL
+ * or only the GPL Version 2, indicate your decision by adding
+ * "[Contributor] elects to include this software in this distribution
+ * under the [CDDL or GPL Version 2] license." If you do not indicate a
+ * single choice of license, a recipient has the option to distribute
+ * your version of this file under either the CDDL, the GPL Version 2 or
+ * to extend the choice of license to its licensees as provided above.
+ * However, if you add GPL Version 2 code and therefore, elected the GPL
+ * Version 2 license, then the option applies only if the new code is
+ * made subject to such option by the copyright holder.
+ *
+ * Contributor(s):
+ *
+ * Portions Copyrighted 2016 Sun Microsystems, Inc.
+ */
+package org.netbeans.modules.spellchecker.bindings.php;
+
+import javax.swing.text.Document;
+import org.netbeans.editor.BaseDocument;
+import org.netbeans.modules.editor.NbEditorUtilities;
+import org.netbeans.modules.php.api.util.FileUtils;
+import org.netbeans.modules.spellchecker.spi.language.TokenList;
+import org.netbeans.modules.spellchecker.spi.language.TokenListProvider;
+import org.openide.util.lookup.ServiceProvider;
+
+@ServiceProvider(service = TokenListProvider.class)
+public class PHPTokenListProvider implements TokenListProvider {
+
+ @Override
+ public TokenList findTokenList(Document doc) {
+ String mimeType = NbEditorUtilities.getMimeType(doc);
+ if (FileUtils.PHP_MIME_TYPE.equals(mimeType) && doc instanceof BaseDocument) {
+ return new PHPTokenList(doc);
+ }
+ return null;
+ }
+
+}
diff --git a/spellchecker.bindings.php/src/org/netbeans/modules/spellchecker/bindings/php/resources/Bundle.properties b/spellchecker.bindings.php/src/org/netbeans/modules/spellchecker/bindings/php/resources/Bundle.properties
new file mode 100644
--- /dev/null
+++ b/spellchecker.bindings.php/src/org/netbeans/modules/spellchecker/bindings/php/resources/Bundle.properties
@@ -0,0 +1,46 @@
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+#
+# Copyright 2016 Oracle and/or its affiliates. All rights reserved.
+#
+# Oracle and Java are registered trademarks of Oracle and/or its affiliates.
+# Other names may be trademarks of their respective owners.
+#
+# The contents of this file are subject to the terms of either the GNU
+# General Public License Version 2 only ("GPL") or the Common
+# Development and Distribution License("CDDL") (collectively, the
+# "License"). You may not use this file except in compliance with the
+# License. You can obtain a copy of the License at
+# http://www.netbeans.org/cddl-gplv2.html
+# or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
+# specific language governing permissions and limitations under the
+# License. When distributing the software, include this License Header
+# Notice in each file and include the License file at
+# nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
+# particular file as subject to the "Classpath" exception as provided
+# by Oracle in the GPL Version 2 section of the License file that
+# accompanied this code. If applicable, add the following below the
+# License Header, with the fields enclosed by brackets [] replaced by
+# your own identifying information:
+# "Portions Copyrighted [year] [name of copyright owner]"
+#
+# If you wish your version of this file to be governed by only the CDDL
+# or only the GPL Version 2, indicate your decision by adding
+# "[Contributor] elects to include this software in this distribution
+# under the [CDDL or GPL Version 2] license." If you do not indicate a
+# single choice of license, a recipient has the option to distribute
+# your version of this file under either the CDDL, the GPL Version 2 or
+# to extend the choice of license to its licensees as provided above.
+# However, if you add GPL Version 2 code and therefore, elected the GPL
+# Version 2 license, then the option applies only if the new code is
+# made subject to such option by the copyright holder.
+#
+# Contributor(s):
+#
+# Portions Copyrighted 2016 Sun Microsystems, Inc.
+OpenIDE-Module-Display-Category=Editing
+OpenIDE-Module-Long-Description=\
+ Support for spellchecker in PHP.
+OpenIDE-Module-Name=Spellchecker PHP Language Bindings
+
+OpenIDE-Module-Short-Description=Spellchecker PHP Language Bindings
+Spellcheckers/Phpdoc=PHPDoc comments
diff --git a/spellchecker.bindings.php/src/org/netbeans/modules/spellchecker/bindings/php/resources/layer.xml b/spellchecker.bindings.php/src/org/netbeans/modules/spellchecker/bindings/php/resources/layer.xml
new file mode 100644
--- /dev/null
+++ b/spellchecker.bindings.php/src/org/netbeans/modules/spellchecker/bindings/php/resources/layer.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spellchecker.bindings.php/test/unit/src/org/netbeans/modules/spellchecker/bindings/php/PHPTokenListTest.java b/spellchecker.bindings.php/test/unit/src/org/netbeans/modules/spellchecker/bindings/php/PHPTokenListTest.java
new file mode 100644
--- /dev/null
+++ b/spellchecker.bindings.php/test/unit/src/org/netbeans/modules/spellchecker/bindings/php/PHPTokenListTest.java
@@ -0,0 +1,245 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2016 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
+ * Other names may be trademarks of their respective owners.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common
+ * Development and Distribution License("CDDL") (collectively, the
+ * "License"). You may not use this file except in compliance with the
+ * License. You can obtain a copy of the License at
+ * http://www.netbeans.org/cddl-gplv2.html
+ * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
+ * specific language governing permissions and limitations under the
+ * License. When distributing the software, include this License Header
+ * Notice in each file and include the License file at
+ * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the GPL Version 2 section of the License file that
+ * accompanied this code. If applicable, add the following below the
+ * License Header, with the fields enclosed by brackets [] replaced by
+ * your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * If you wish your version of this file to be governed by only the CDDL
+ * or only the GPL Version 2, indicate your decision by adding
+ * "[Contributor] elects to include this software in this distribution
+ * under the [CDDL or GPL Version 2] license." If you do not indicate a
+ * single choice of license, a recipient has the option to distribute
+ * your version of this file under either the CDDL, the GPL Version 2 or
+ * to extend the choice of license to its licensees as provided above.
+ * However, if you add GPL Version 2 code and therefore, elected the GPL
+ * Version 2 license, then the option applies only if the new code is
+ * made subject to such option by the copyright holder.
+ *
+ * Contributor(s):
+ *
+ * Portions Copyrighted 2016 Sun Microsystems, Inc.
+ */
+package org.netbeans.modules.spellchecker.bindings.php;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import javax.swing.text.Document;
+import javax.swing.text.PlainDocument;
+import static junit.framework.TestCase.assertEquals;
+import org.junit.Test;
+import org.netbeans.api.lexer.Language;
+import org.netbeans.junit.NbTestCase;
+import org.netbeans.modules.php.editor.lexer.PHPTokenId;
+import org.netbeans.modules.spellchecker.spi.language.TokenList;
+
+/**
+ * Based on JavaTokenListTest.
+ */
+public class PHPTokenListTest extends NbTestCase {
+
+ private static final String PHPTAG = "test
testt
testttt testttttt*/",
+ "tes", "testttttt"
+ );
+ }
+
+ public void testMultiLineComment() throws Exception {
+ tokenListTest(
+ " /*\n"
+ + " * test description.\n"
+ + " */"
+ );
+ }
+
+ public void testSimplewriting() throws Exception {
+ tokenListTestWithWriting(
+ "/** tes test*/ testt testtt /*testttt*//** testtttt*//** testttttt*/",
+ 15, "bflmpsvz", 14,
+ "testtttt", "testttttt"
+ );
+ }
+
+ public void testDot() throws Exception {
+ tokenListTest(
+ "/** tes.test */",
+ "tes", "test"
+ );
+ }
+
+ public void testTagHandling() throws Exception {
+ tokenListTest(
+ " /**\n"
+ + " * @see http://example.com/my/bar/qwertyuio aaa.\n"
+ + " * @see MyClass::$items aab .\n"
+ + " * @author abi abj abk abl\n"
+ + " * @something array|int somethingdesc.\n"
+ + " */",
+ "aaa", "aab", "array", "int", "somethingdesc"
+ );
+ }
+
+ public void testTypeTagHandling() throws Exception {
+ tokenListTest(
+ " /**\n"
+ + " * @param string|int $a paramdesc.\n"
+ + " * @param string|int $b \n"
+ + " * @property MyClass $myClass propertydesc.\n"
+ + " * @property-read Foo $foo propertyrdesc.\n"
+ + " * @property-write Bar $bar propertywdesc.\n"
+ + " * @var array vardesc\n"
+ + " * @return $this|int returndesc\n"
+ + " */",
+ "paramdesc", "propertydesc", "propertyrdesc", "propertywdesc", "vardesc", "returndesc"
+ );
+ }
+
+ public void testMethodTagHandling() throws Exception {
+ tokenListTest(
+ " /**\n"
+ + " * @method int aaa() methoddesca.\n"
+ + " * @method int aaa_bbb() methoddescb.\n"
+ + " * @method int aaa_bbb2() methoddescc.\n"
+ + " * @method int bbb(array $a, $b) methoddescd.\n"
+ + " * @method string[] ccc() ccc(array $a, $b) methoddesce.\n"
+ + " */",
+ "methoddesca", "methoddescb", "methoddescc", "methoddescd", "methoddesce"
+ );
+ }
+
+ public void testLinkHandling() throws Exception {
+ tokenListTest(
+ "/** {@link aba abb abc} {abd }abe*/",
+ "abd", "abe"
+ );
+ }
+
+ public void testInheritdocHandling() throws Exception {
+ tokenListTest(
+ "/** {@inheritdoc} child. */",
+ "child"
+ );
+ }
+
+ public void testVariableHandling() throws Exception {
+ tokenListTest(
+ "/** something $var variable $var_name */",
+ "something", "variable"
+ );
+ }
+
+ public void testEntities() throws Exception {
+ tokenListTest(
+ "/** > > */"
+ );
+ }
+
+ public void testPositions() throws Exception {
+ Document doc = new PlainDocument();
+ doc.putProperty(Language.class, PHPTokenId.language());
+ doc.insertString(0, "testt testttt testttttt*/", null);
+ TokenList tokenList = new PHPTokenList(doc);
+ tokenList.setStartOffset(16); // words = new ArrayList<>();
+ TokenList tokenList = new PHPTokenList(doc);
+ tokenList.setStartOffset(0);
+ while (tokenList.nextWord()) {
+ words.add(tokenList.getCurrentWordText().toString());
+ }
+ assertEquals(Arrays.asList(golden), words);
+ }
+
+ /**
+ * Check a token list with writing.
+ *
+ * @param documentContent
+ * @param offset
+ * @param text
+ * @param startOffset
+ * @param golden expected words
+ * @throws Exception
+ */
+ private void tokenListTestWithWriting(String documentContent, int offset, String text, int startOffset, String... golden) throws Exception {
+ Document doc = new PlainDocument();
+ doc.putProperty(Language.class, PHPTokenId.language());
+ doc.insertString(0, PHPTAG + documentContent, null);
+ List words = new ArrayList<>();
+ TokenList tokenList = new PHPTokenList(doc);
+ while (tokenList.nextWord()) {
+ }
+
+ doc.insertString(offset + PHPTAG.length(), text, null);
+ tokenList.setStartOffset(startOffset + PHPTAG.length());
+ while (tokenList.nextWord()) {
+ words.add(tokenList.getCurrentWordText().toString());
+ }
+ assertEquals(Arrays.asList(golden), words);
+ }
+}