blob: 35a24f76ddba32ce7dc6d6b216f5493b7d2aa7d2 [file] [log] [blame]
/*
* 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.javascript2.editor.embedding;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.MatchResult;
import org.netbeans.api.html.lexer.HTMLTokenId;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenId;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.lib.editor.util.CharSequenceUtilities;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.parsing.api.Embedding;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.parsing.spi.EmbeddingProvider;
import org.netbeans.modules.parsing.spi.SchedulerTask;
import org.netbeans.modules.parsing.spi.TaskFactory;
/**
*
* @see "org.netbeans.modules.parsing.impl.SourceCache.resortTaskFactories() which
* depends on fully qualified name of this class Factory. See issue #162990 for more information."
*
* @author vita, mfukala@netbeans.org
*/
public final class JsEmbeddingProvider extends EmbeddingProvider {
private static final int PRIORITY = 0; //First one
private static final String JS_MIMETYPE = "text/javascript"; // NOI18N
private static final String SCRIPT_TYPE_MODULE = "module"; // NOI18N
public static final String NETBEANS_IMPORT_FILE = "__netbeans_import__"; // NOI18N
// ------------------------------------------------------------------------
// EmbeddingProvider implementation
// ------------------------------------------------------------------------
@Override
public List<Embedding> getEmbeddings(Snapshot snapshot) {
if (sourceMimeType.equals(snapshot.getMimeType())) {
List<Embedding> embeddings = translator.translate(snapshot);
if (embeddings.isEmpty()) {
return Collections.<Embedding>emptyList();
} else {
return Collections.singletonList(Embedding.create(embeddings));
}
} else {
LOG.log(Level.WARNING,
"Unexpected snapshot type: ''{0}''; expecting ''{1}''", //NOI18N
new Object[]{snapshot.getMimeType(), sourceMimeType});
return Collections.<Embedding>emptyList();
}
}
@Override
public int getPriority() {
return PRIORITY;
}
@Override
public void cancel() {
// XXX: ignored at the moment, fix it
}
// ------------------------------------------------------------------------
// TaskFactory implementation
// ------------------------------------------------------------------------
public static final class Factory extends TaskFactory {
public Factory() {
// no-op
}
@Override
public Collection<? extends SchedulerTask> create(Snapshot snapshot) {
//filter out transitive embedding creations like JSP -> HTML -> JavaScript
//we have a direct translator for them JSP -> JavaScript
if (snapshot.getMimeType().equals("text/html") && snapshot.getMimePath().size() > 1) { //NOI18N
return null;
}
Translator t = translators.get(snapshot.getMimeType());
if (t != null) {
return Collections.singleton(new JsEmbeddingProvider(snapshot.getMimeType(), t));
} else if (snapshot.getMimeType().equals("text/javascript")){
return Collections.singleton(new JsEmbeddingProvider(snapshot.getMimeType(), translators.get(JSX_MIME_TYPE)));
} else {
return Collections.<SchedulerTask>emptyList();
}
}
} // End of Factory class
// ------------------------------------------------------------------------
// Public implementation
// ------------------------------------------------------------------------
public static boolean isGeneratedIdentifier(String ident) {
return GENERATED_IDENTIFIER.equals(ident) || ident.contains(NETBEANS_IMPORT_FILE);
}
public static boolean containsGeneratedIdentifier(String ident) {
return ident.contains(GENERATED_IDENTIFIER) || ident.contains(NETBEANS_IMPORT_FILE);
}
// ------------------------------------------------------------------------
// Private implementation
// ------------------------------------------------------------------------
private static final Logger LOG = Logger.getLogger(JsEmbeddingProvider.class.getName());
// private static final String JSP_MIME_TYPE = "text/x-jsp"; // NOI18N
// private static final String TAG_MIME_TYPE = "text/x-tag"; // NOI18N
private static final String RHTML_MIME_TYPE = "application/x-httpd-eruby"; // NOI18N
private static final String HTML_MIME_TYPE = "text/html"; // NOI18N
// private static final String XHTML_MIME_TYPE = "text/xhtml"; // NOI18N
// private static final String PHP_MIME_TYPE = "text/x-php5"; // NOI18N
private static final String TPL_MIME_TYPE = "text/x-tpl"; // NOI18N
private static final String TWIG_MIME_TYPE = "text/x-twig"; // NOI18N
private static final String LATTE_MIME_TYPE = "text/x-latte"; // NOI18N
private static final String JSX_MIME_TYPE = "text/x-jsx"; //NOI18N
//private static final String GSP_TAG_MIME_TYPE = "application/x-gsp"; // NOI18N
private static final Map<String, Translator> translators = new HashMap<>();
static {
// translators.put(JSP_MIME_TYPE, new JspTranslator());
// translators.put(TAG_MIME_TYPE, new JspTranslator());
translators.put(RHTML_MIME_TYPE, new RhtmlTranslator());
//the creation of javascript virtual source for files with text/html mimetype
//is now handled by o.n.m.html.editor.embedding.JsEmbeddingProvider
// translators.put(HTML_MIME_TYPE, new HtmlTranslator());
// translators.put(XHTML_MIME_TYPE, new XhtmlTranslator());
// translators.put(PHP_MIME_TYPE, new PhpTranslator());
translators.put(TPL_MIME_TYPE, new TplTranslator());
translators.put(TWIG_MIME_TYPE, new TwigTranslator());
translators.put(LATTE_MIME_TYPE, new LatteTranslator());
translators.put(JSX_MIME_TYPE, new JsxJsTranslator());
}
// If you change this, update the testcase reference
private static final String GENERATED_IDENTIFIER = "__UNKNOWN__"; // NOI18N
/** PHPTokenId's T_INLINE_HTML name */
private static final String T_INLINE_HTML = "T_INLINE_HTML";
private final String sourceMimeType;
private final Translator translator;
private JsEmbeddingProvider(String sourceMimeType, Translator translator) {
this.sourceMimeType = sourceMimeType;
this.translator = translator;
}
protected interface Translator {
public List<Embedding> translate(Snapshot snapshot);
}
private static final class JspTranslator implements Translator {
/** Create a JavaScript model of the given JSP buffer.
* @todo Make this more general purpose (so it can be used from HTML, JSP etc.)
* @param outputBuffer The buffer to emit the translation to
* @param tokenHierarchy The token hierarchy for the RHTML code
* @param tokenSequence The token sequence for the RHTML code
*/
@Override
public List<Embedding> translate(Snapshot snapshot) {
TokenHierarchy<?> th = snapshot.getTokenHierarchy();
if (th == null) {
//the token hierarchy may be null if the language is not initialized yet
//for example if ergonomics is used and j2ee cluster not activated
return Collections.emptyList();
}
TokenSequence<? extends TokenId> tokenSequence = th.tokenSequence();
List<Embedding> embeddings = new ArrayList<>();
//TODO - implement the "classpath" import for other projects
//how is the javascript classpath done????????/
JsAnalyzerState state = new JsAnalyzerState();
while (tokenSequence.moveNext()) {
Token<? extends TokenId> token = tokenSequence.token();
if (token.id().primaryCategory().equals("text")) { // NOI18N
TokenSequence<? extends HTMLTokenId> ts = tokenSequence.embedded(HTMLTokenId.language());
if (ts == null) {
continue;
}
extractJavaScriptFromHtml(snapshot, ts, state, embeddings);
} else if (token.id().primaryCategory().equals("expression-language") || token.id().primaryCategory().equals("scriptlet") || token.id().primaryCategory().equals("symbol") && "/>".equals(token.text().toString())) { // NOI18N
//The test for jsp /> symbol means
//that we just encountered an end of jsp tag without body
//so it is possible/likely the tag generates something
//TODO Add a list of know tags and adjust the heuristics according
//to the tag declaration. It may work nicely using the
//JSP parser for getting custom jsp tags metadata.
//TODO The whole implementation of the JsJspModel
//should be in separate file in JSP module and should
//depend on both lexers instead of these string dependencies.
if (state.in_inlined_javascript || state.in_javascript) {
embeddings.add(snapshot.create(GENERATED_IDENTIFIER, JsTokenId.JAVASCRIPT_MIME_TYPE));
// embeddings.add(snapshot.create("/*", JsTokenId.JAVASCRIPT_MIME_TYPE));
// embeddings.add(snapshot.create(tokenSequence.offset(), token.length() , JsTokenId.JAVASCRIPT_MIME_TYPE));
// embeddings.add(snapshot.create("*/", JsTokenId.JAVASCRIPT_MIME_TYPE));
}
}
}
return embeddings;
}
} // End JspTranslator class
private static final class XhtmlTranslator implements Translator {
@Override
public List<Embedding> translate(Snapshot snapshot) {
TokenHierarchy<?> th = snapshot.getTokenHierarchy();
if (th == null) {
//the token hierarchy may be null if the language is not initialized yet
//for example if ergonomics is used and j2ee cluster not activated
return Collections.emptyList();
}
TokenSequence<? extends TokenId> tokenSequence = th.tokenSequence();
List<Embedding> embeddings = new ArrayList<>();
JsAnalyzerState state = new JsAnalyzerState();
while (tokenSequence.moveNext()) {
Token<? extends TokenId> token = tokenSequence.token();
if (token.id().primaryCategory().equals("html")) { // NOI18N
TokenSequence<? extends HTMLTokenId> ts = tokenSequence.embedded(HTMLTokenId.language());
if (ts == null) {
continue;
}
extractJavaScriptFromHtml(snapshot, ts, state, embeddings);
} else if (token.id().primaryCategory().equals("expression-language") ) { // NOI18N
if (state.in_inlined_javascript || state.in_javascript) {
embeddings.add(snapshot.create(GENERATED_IDENTIFIER, JsTokenId.JAVASCRIPT_MIME_TYPE));
}
}
}
return embeddings;
}
}
private static final class PhpTranslator implements Translator {
@Override
public List<Embedding> translate(Snapshot snapshot) {
TokenHierarchy<?> th = snapshot.getTokenHierarchy();
if (th == null) {
//likely the php language couldn't be found
LOG.log(Level.INFO,
"Cannot get TokenHierarchy from snapshot {0}", //NOI18N
snapshot);
return Collections.emptyList();
}
TokenSequence<? extends TokenId> tokenSequence = th.tokenSequence();
List<Embedding> embeddings = new ArrayList<>();
//TODO - implement the "classpath" import for other projects
//how is the javascript classpath done????????/
JsAnalyzerState state = new JsAnalyzerState();
while (tokenSequence.moveNext()) {
Token<? extends TokenId> token = tokenSequence.token();
if (token.id().name().equals(T_INLINE_HTML)) { // NOI18N
TokenSequence<? extends HTMLTokenId> ts = tokenSequence.embedded(HTMLTokenId.language());
if (ts == null) {
continue;
}
extractJavaScriptFromHtml(snapshot, ts, state, embeddings);
}
if (state.in_inlined_javascript || state.in_javascript) {
//find end of the php code
boolean wasInPhp = false;
boolean hasNext;
while ((hasNext = tokenSequence.moveNext()) && !tokenSequence.token().id().name().equals(T_INLINE_HTML)) {
wasInPhp = true;
}
if (hasNext) { //do not move back if we are at the end of the sequence = cycle!
//we are out of php code, lets move back to the previous token
tokenSequence.movePrevious();
}
if (wasInPhp) {
embeddings.add(snapshot.create(GENERATED_IDENTIFIER, JsTokenId.JAVASCRIPT_MIME_TYPE));
}
}
}
return embeddings;
}
} // End of PhpTranslator class
protected static final class TplTranslator implements Translator {
private static final String T_HTML = "T_HTML"; //NOI18N
private static final String T_SMARTY = "T_SMARTY"; //NOI18N
@Override
public List<Embedding> translate(Snapshot snapshot) {
TokenHierarchy<?> th = snapshot.getTokenHierarchy();
if (th == null) {
//likely the tpl language couldn't be found
LOG.log(Level.INFO,
"Cannot get TokenHierarchy from snapshot {0}", //NOI18N
snapshot);
return Collections.emptyList();
}
TokenSequence<? extends TokenId> tokenSequence = th.tokenSequence();
List<Embedding> embeddings = new ArrayList<>();
//TODO - implement the "classpath" import for other projects
//how is the javascript classpath done????????/
JsAnalyzerState state = new JsAnalyzerState();
while (tokenSequence.moveNext()) {
Token<? extends TokenId> token = tokenSequence.token();
if (token.id().name().equals(T_HTML)) {
TokenSequence<? extends HTMLTokenId> ts = tokenSequence.embedded(HTMLTokenId.language());
if (ts == null) {
continue;
}
extractJavaScriptFromHtml(snapshot, ts, state, embeddings);
} else {
if (state.in_inlined_javascript || state.in_javascript) {
//find end of the tpl code
boolean wasInTpl = false;
boolean wasInLiteral = false;
boolean hasNext;
while (hasNext = tokenSequence.moveNext()) {
Token<? extends TokenId> innerToken = tokenSequence.token();
if (CharSequenceUtilities.textEquals("ldelim", innerToken.text())) { //NOI18N
wasInLiteral = true;
embeddings.add(snapshot.create("{", JsTokenId.JAVASCRIPT_MIME_TYPE)); //NOI18N
} else if (CharSequenceUtilities.textEquals("rdelim", innerToken.text())) { //NOI18N
wasInLiteral = true;
embeddings.add(snapshot.create("}", JsTokenId.JAVASCRIPT_MIME_TYPE)); //NOI18N
} else if (!innerToken.id().name().equals(T_HTML)) {
wasInTpl = true;
if (CharSequenceUtilities.indexOf(innerToken.text(), "literal") > -1) { //NOI18N
wasInLiteral = true;
} else if (innerToken.id().name().equals(T_SMARTY) && wasInLiteral) {
wasInLiteral = false;
embeddings.add(snapshot.create(GENERATED_IDENTIFIER, JsTokenId.JAVASCRIPT_MIME_TYPE));
}
} else {
break;
}
}
if (hasNext) { //do not move back if we are at the end of the sequence = cycle!
//we are out of tpl code, lets move back to the previous token
tokenSequence.movePrevious();
}
if (wasInTpl && !wasInLiteral) {
embeddings.add(snapshot.create(GENERATED_IDENTIFIER, JsTokenId.JAVASCRIPT_MIME_TYPE));
}
}
}
}
return embeddings;
}
} // End of TplTranslator class
private static final class TwigTranslator implements Translator {
@Override
public List<Embedding> translate(Snapshot snapshot) {
TokenHierarchy<?> th = snapshot.getTokenHierarchy();
if (th == null) {
//likely the twig language couldn't be found
LOG.log(Level.INFO,
"Cannot get TokenHierarchy from snapshot {0}", //NOI18N
snapshot);
return Collections.emptyList();
}
TokenSequence<? extends TokenId> tokenSequence = th.tokenSequence();
List<Embedding> embeddings = new ArrayList<>();
JsAnalyzerState state = new JsAnalyzerState();
while (tokenSequence.moveNext()) {
Token<? extends TokenId> token = tokenSequence.token();
if (token.id().name().equals("T_HTML") || token.id().name().equals("T_TWIG_RAW")) { //NOI18N
TokenSequence<? extends HTMLTokenId> ts = tokenSequence.embedded(HTMLTokenId.language());
if (ts == null) {
continue;
}
extractJavaScriptFromHtml(snapshot, ts, state, embeddings);
} else if (token.id().name().equals("T_TWIG")) { // NOI18N
if (state.in_inlined_javascript || state.in_javascript) {
//find end of the twig
boolean hasNext = false;
while (token.id().name().equals("T_TWIG")) { // NOI18N
hasNext = tokenSequence.moveNext();
if (!hasNext) {
break;
}
token = tokenSequence.token();
}
if (hasNext) {
tokenSequence.movePrevious();
}
embeddings.add(snapshot.create(GENERATED_IDENTIFIER, JsTokenId.JAVASCRIPT_MIME_TYPE));
}
}
}
return embeddings;
}
} // End of TwigTranslator class
private static final class LatteTranslator implements Translator {
@Override
public List<Embedding> translate(Snapshot snapshot) {
TokenHierarchy<?> th = snapshot.getTokenHierarchy();
if (th == null) {
//likely the latte language couldn't be found
LOG.log(Level.INFO,
"Cannot get TokenHierarchy from snapshot {0}", //NOI18N
snapshot);
return Collections.emptyList();
}
TokenSequence<? extends TokenId> tokenSequence = th.tokenSequence();
List<Embedding> embeddings = new ArrayList<>();
JsAnalyzerState state = new JsAnalyzerState();
while (tokenSequence.moveNext()) {
Token<? extends TokenId> token = tokenSequence.token();
if (token.id().name().equals("T_HTML")) { //NOI18N
TokenSequence<? extends HTMLTokenId> ts = tokenSequence.embedded(HTMLTokenId.language());
if (ts == null) {
continue;
}
extractJavaScriptFromHtml(snapshot, ts, state, embeddings);
} else if (token.id().name().equals("T_LATTE")) { // NOI18N
if (state.in_inlined_javascript || state.in_javascript) {
//find end of the latte
boolean hasNext = false;
while (token.id().name().equals("T_LATTE")) { // NOI18N
hasNext = tokenSequence.moveNext();
if (!hasNext) {
break;
}
token = tokenSequence.token();
}
if (hasNext) {
tokenSequence.movePrevious();
}
embeddings.add(snapshot.create(GENERATED_IDENTIFIER, JsTokenId.JAVASCRIPT_MIME_TYPE));
}
}
}
return embeddings;
}
} // End of LatteTranslator class
private static final class RhtmlTranslator implements Translator {
@Override
public List<Embedding> translate(Snapshot snapshot) {
List<Embedding> embeddings = new ArrayList<>();
TokenHierarchy<?> th = snapshot.getTokenHierarchy();
if (th == null) {
//likely a rhtml language couldn't be found
LOG.log(Level.INFO,
"Cannot get TokenHierarchy from snapshot {0}", //NOI18N
snapshot);
return embeddings;
}
TokenSequence<? extends TokenId> tokenSequence = th.tokenSequence();
// Add a super class such that code completion, goto declaration etc.
// knows where to pull the various link_to etc. methods from
// // Pretend that this code is an extension to ActionView::Base such that
// // code completion, go to declaration etc. sees the inherited methods from
// // ActionView -- link_to and friends.
// buffer.append("class ActionView::Base\n"); // NOI18N
// // TODO Try to include the helper class as well as the controller fields too;
// // for now this logic is hardcoded into Js's code completion engine (CodeCompleter)
//
// // Erubis uses _buf; I've seen eruby using something else (_erbout?)
// buffer.append("_buf='';"); // NOI18N
// codeBlocks.add(new CodeBlockData(0, 0, 0, buffer.length()));
/* This could be a huge bottleneck - see http://www.netbeans.org/issues/show_bug.cgi?id=134329
FileObject fo = GsfUtilities.findFileObject(doc);
if (fo != null) {
Project project = FileOwnerQuery.getOwner(fo);
if (project != null) {
StringBuilder sb = new StringBuilder();
FileObject javascriptFolder = project.getProjectDirectory().getFileObject("public/javascripts"); // NOI18N
if (javascriptFolder != null) {
addJavaScriptFiles(javascriptFolder, sb);
if (sb.length() > 0) {
// Insert a file link
//int sourceStart = ts.offset();
int sourceStart = 0;
String path = sb.toString();
String insertText = JsAnalyzer.NETBEANS_IMPORT_FILE + "(" + path + ");\n"; // NOI18N
// This corresponds to a 0-size block in the source
int sourceEnd = sourceStart;
int generatedStart = buffer.length();
buffer.append(insertText);
int generatedEnd = buffer.length();
CodeBlockData blockData = new CodeBlockData(sourceStart, sourceEnd, generatedStart,
generatedEnd);
codeBlocks.add(blockData);
}
}
}
}
*/
JsAnalyzerState state = new JsAnalyzerState();
// Rails automatically inserts certain JavaScript libraries:
// <script type="text/javascript" src="javascripts/prototype.js"></script>
// <script type="text/javascript" src="javascripts/effects.js"></script>
// I need to insert includes for these
// Actually, see
// actionpack-2.0.2/lib/action_view/helpers/asset_tag_helper.rb
// and #javascript_include_tag in particular for details on inclusions.
// I need to go and model these guys - which could come from layout
// pages etc.
//./app/views/admin/themes/index.rhtml:2:<%= javascript_include_tag 'mephisto/dialog' %>
//./app/views/layouts/application.rhtml:8: <%= javascript_include_tag 'mephisto/prototype', 'mephisto/effects', 'mephisto/dragdrop', 'mephisto/lowpro', 'mephisto/application' %>
//./app/views/layouts/simple.rhtml:8: <%= javascript_include_tag 'mephisto/prototype', 'mephisto/effects', 'mephisto/lowpro', 'mephisto/application' %>
// For now, just recursively include all the JavaScript files in public/javascript/
//FileObject fo =
while (tokenSequence.moveNext()) {
Token<? extends TokenId> token = tokenSequence.token();
// Conversion algorithm:
// Translate <html> content into into variables assigned to the right types?
// e.g. anything with "id" should be set, as should <TBD>
// Concatenate <script> blocks verbatim
// In Ruby code, look for javascript_include_tag :defaults
// which will include the files in rails' public/javascripts/.
// Handle <link> script includes. Should I worry about anything else ("this" file
// being run in the context of something else).
// TODO: Ruby - look for embedded Ruby sections like this:
// ./admin/users/_toggles.rhtml:3: <%= check_box_tag user.dom_id(:user_toggle), user.id, user.deleted_at.nil?, :onclick => "UserForm.toggle(this);" %>
if (token.id().primaryCategory().equals("html")) { // NOI18N
TokenSequence<? extends HTMLTokenId> ts = tokenSequence.embedded(HTMLTokenId.language());
if (ts == null) {
continue;
}
extractJavaScriptFromHtml(snapshot, ts, state, embeddings);
} else if (token.id().primaryCategory().equals("ruby")) { // NOI18N
// TODO - look for JavaScript context in Ruby, like this onclick handler:
// <%= submit_tag 'Upload', :onclick => "Element.show('upload-spinner')" %>
// TODO insert missing portions in embedded javascript,
// example1: <script>puts "Hello <%= get_name %>"</script>
// example2: <input id="<%= id %>">
// TODO - make sure it's NOT an erb comment!!!
if (state.in_inlined_javascript || state.in_javascript) {
int sourceStart = tokenSequence.offset();
int sourceEnd = sourceStart + token.length();
embeddings.add(snapshot.create(GENERATED_IDENTIFIER, JsTokenId.JAVASCRIPT_MIME_TYPE));
// embeddings.add(snapshot.create("/*", JsTokenId.JAVASCRIPT_MIME_TYPE));
// embeddings.add(snapshot.create(sourceStart, sourceEnd - sourceStart, JsTokenId.JAVASCRIPT_MIME_TYPE));
// embeddings.add(snapshot.create("*/", JsTokenId.JAVASCRIPT_MIME_TYPE));
}
}
}
// // Close off the class
// // eruby also ends with this statement: _buf.to_s
// String end = "\nend\n"; // NOI18N
// buffer.append(end);
// if (doc != null) {
// codeBlocks.add(new CodeBlockData(doc.getLength(), doc.getLength(), buffer.length()-end.length(), buffer.length()));
// }
return embeddings;
}
} // End of RhtmlTranslator class
private static class JsxJsTranslator implements Translator {
private static final int MAX_EMBEDDING_LENGTH = 5000000; //cca 5M
public static final String GENERATED_CODE = "@@@"; //NOI18N
@Override
public List<Embedding> translate(Snapshot snapshot) {
TokenHierarchy<?> th = snapshot.getTokenHierarchy();
if (th == null) {
return Collections.<Embedding>emptyList();
}
TokenSequence<JsTokenId> sequence = th.tokenSequence(JsTokenId.javascriptLanguage());
sequence.moveStart();
List<Embedding> embeddings = new ArrayList<>();
//marek (workaround): there seems to be a bug in parsing api - if I create
//the embedding for each PHPTokenId.T_INLINE_HTML token separatelly then the offsets
//translation is broken
int from = -1;
int len = 0;
while (sequence.moveNext()) {
Token t = sequence.token();
if (t.id() == JsTokenId.JSX_TEXT) {
if (from < 0) {
from = sequence.offset();
}
len += t.length();
} else {
if (from >= 0) {
//lets suppose the text is always html :-(
createHtmlEmbedding(embeddings, snapshot, from, len);
//add only one virtual generated token for a sequence of PHP tokens
embeddings.add(snapshot.create(GENERATED_CODE, HTML_MIME_TYPE));
}
from = -1;
len = 0;
}
}
if (from >= 0) {
createHtmlEmbedding(embeddings, snapshot, from, len);
}
if (!embeddings.isEmpty()) {
return Collections.singletonList(Embedding.create(embeddings));
}
return Collections.<Embedding>emptyList();
}
private static void createHtmlEmbedding(List<Embedding> embeddings, Snapshot snapshot, int from, int length) {
assert embeddings != null;
assert snapshot != null;
if (length <= MAX_EMBEDDING_LENGTH) {
embeddings.add(snapshot.create(from, length, HTML_MIME_TYPE)); //NOI18N
}
}
}
private static final class HtmlTranslator implements Translator {
@Override
public List<Embedding> translate(Snapshot snapshot) {
TokenSequence<? extends TokenId> tokenSequence = snapshot.getTokenHierarchy().tokenSequence();
List<Embedding> embeddings = new ArrayList<>();
JsAnalyzerState state = new JsAnalyzerState();
@SuppressWarnings("unchecked")
TokenSequence<? extends HTMLTokenId> htmlTokenSequence = (TokenSequence<? extends HTMLTokenId>) tokenSequence;
extractJavaScriptFromHtml(snapshot, htmlTokenSequence, state, embeddings);
return embeddings;
}
} // End of HtmlTranslator class
/** @return True if we're still in the middle of an embedded token */
private static void extractJavaScriptFromHtml(Snapshot snapshot, TokenSequence<? extends HTMLTokenId> ts, JsAnalyzerState state, List<Embedding> embeddings) {
// NOI18N
// Process the HTML content: look for embedded script blocks,
// as well as JavaScript event handlers in element attributes.
ts.moveStart();
tokens:
while (ts.moveNext()) {
Token<? extends HTMLTokenId> htmlToken = ts.token();
HTMLTokenId htmlId = htmlToken.id();
if (htmlId == HTMLTokenId.SCRIPT) {
String scriptType = (String)htmlToken.getProperty(HTMLTokenId.SCRIPT_TYPE_TOKEN_PROPERTY);
if(scriptType == null || JS_MIMETYPE.equals(scriptType) || SCRIPT_TYPE_MODULE.equals(scriptType)) {
state.in_javascript = true;
// Emit the block verbatim
int sourceStart = ts.offset();
String text = htmlToken.text().toString();
List<EmbeddingPosition> jsEmbeddings = extractJsEmbeddings(text, sourceStart);
for (EmbeddingPosition embedding : jsEmbeddings) {
embeddings.add(snapshot.create(embedding.getOffset(), embedding.getLength(), JsTokenId.JAVASCRIPT_MIME_TYPE));
}
// XXX: need better support in parsing api for this
// embeddings.add(snapshot.create("/*", JsTokenId.JAVASCRIPT_MIME_TYPE));
// embeddings.add(snapshot.create(sourceStart, sourceEnd - sourceStart, JsTokenId.JAVASCRIPT_MIME_TYPE));
// embeddings.add(snapshot.create("*/", JsTokenId.JAVASCRIPT_MIME_TYPE));
// int generatedStart = buffer.length();
// buffer.append(text);
// int generatedEnd = buffer.length();
// CodeBlockData blockData = new CodeBlockData(sourceStart, sourceEnd, generatedStart,
// generatedEnd);
// codeBlocks.add(blockData);
}
} else if (htmlId == HTMLTokenId.TAG_OPEN) {
String text = htmlToken.text().toString();
// TODO - if we see a <script src="someurl"> block that also
// has a nonempty body, warn - the body will be ignored!!
// (This should be a quickfix)
if ("script".equals(text)) {
// Look for "<script src=" and if found, locate any includes.
// Quit when I find TAG_CLOSE or run out of tokens
// (for files with errors)
TokenSequence<? extends HTMLTokenId> ets = ts.subSequence(ts.offset());
ets.moveStart();
boolean foundSrc = false;
boolean foundType = false;
String type = null;
String src = null;
while (ets.moveNext()) {
Token<? extends HTMLTokenId> t = ets.token();
HTMLTokenId id = t.id();
// TODO - if we see a DEFER attribute here record that somehow
// such that I can have a quickfix look to make sure you don't try
// to mess with the document!
if (id == HTMLTokenId.TAG_CLOSE_SYMBOL) {
break;
} else if (foundSrc || foundType) {
if (id == HTMLTokenId.ARGUMENT) {
break;
} else if (id == HTMLTokenId.VALUE) {
// Found a script src
if (foundSrc) {
src = t.toString();
} else {
assert foundType;
type = t.toString();
}
foundSrc = false;
foundType = false;
}
} else if (id == HTMLTokenId.ARGUMENT) {
String val = t.toString();
if ("src".equals(val)) {
foundSrc = true;
} else if ("type".equals(val)) {
foundType = true;
}
}
}
if (src != null) {
if (type == null || type.toLowerCase().contains("javascript")) {
if (src.length() > 2 && src.startsWith("\"") && src.endsWith("\"")) {
src = src.substring(1, src.length() - 1);
}
if (src.length() > 2 && src.startsWith("'") && src.endsWith("'")) {
src = src.substring(1, src.length() - 1);
}
// Insert a file link
String insertText = NETBEANS_IMPORT_FILE + "('" + src + "');\n"; // NOI18N
embeddings.add(snapshot.create(insertText, JsTokenId.JAVASCRIPT_MIME_TYPE));
}
}
}
} else if (state.in_javascript && htmlId == HTMLTokenId.TEXT) {
embeddings.add(snapshot.create(ts.offset(), htmlToken.length(), JsTokenId.JAVASCRIPT_MIME_TYPE));
} else if (htmlId == HTMLTokenId.VALUE_JAVASCRIPT) {
int sourceStart = ts.offset();
int sourceEnd = sourceStart + ts.token().length();
if (!state.in_inlined_javascript) {
//first inlined javascript token
String value = htmlToken.text().toString();
// Strip opening "'s
if (value.length() > 0) {
char fch = value.charAt(0); //get first char
if (fch == '\'' || fch == '"') {
state.opening_quotation_stripped = true;
sourceStart++; //skip the quotation
}
}
//first inlined JS section - add the prelude
// Add a function context around the event handler
// such that it gets proper function context (e.g.
// it can return values, the way event handlers can)
embeddings.add(snapshot.create("(function(){\n", JsTokenId.JAVASCRIPT_MIME_TYPE)); //NOI18N
}
state.in_inlined_javascript = true;
state.lastInlinedJavascriptToken = ts.token();
state.lastInlinedJavscriptEmbedding = snapshot.create(sourceStart, sourceEnd - sourceStart, JsTokenId.JAVASCRIPT_MIME_TYPE);
//add the embedding
embeddings.add(state.lastInlinedJavscriptEmbedding);
state.inlined_javascript_pieces++;
} else if (state.in_inlined_javascript && htmlId != HTMLTokenId.VALUE_JAVASCRIPT) {
//we left the inlined javascript section
//need to check if the last inlined javascript section endded
//with a quotation and if so, strip it from the virtual source
assert state.lastInlinedJavscriptEmbedding != null;
assert state.lastInlinedJavascriptToken != null;
int sourceStart = state.lastInlinedJavascriptToken.offset(snapshot.getTokenHierarchy());
int sourceLength = state.lastInlinedJavascriptToken.length();
CharSequence value = state.lastInlinedJavascriptToken.text();
//strip closing quotation
if (state.opening_quotation_stripped) {
if (value.length() > 0) {
char fch = value.charAt(value.length() - 1);
if (fch == '\'' || fch == '"') {
sourceLength--;
//if there is only one inlined javascript piece, and starting quotation has been stripped,
//we need to do that again in the reentered embedding
if (state.inlined_javascript_pieces == 1) {
sourceStart++;
sourceLength--;
}
//need to adjust the last embedding
//1. remove the embedding from the list
boolean removed = embeddings.remove(state.lastInlinedJavscriptEmbedding);
assert removed;
//2. create new embedding with the adjusted length
embeddings.add(snapshot.create(sourceStart, sourceLength, JsTokenId.JAVASCRIPT_MIME_TYPE));
}
}
}
//end of inlined javascript section - add postlude
state.in_inlined_javascript = false;
state.opening_quotation_stripped = false;
state.lastInlinedJavascriptToken = null;
state.lastInlinedJavscriptEmbedding = null;
state.inlined_javascript_pieces = 0;
// Finish the surrounding function context
embeddings.add(snapshot.create("\n});\n", JsTokenId.JAVASCRIPT_MIME_TYPE)); //NOI18N
} else if (htmlId == HTMLTokenId.TAG_CLOSE && "script".equals(htmlToken.toString())) {
embeddings.add(snapshot.create("\n", JsTokenId.JAVASCRIPT_MIME_TYPE)); //NOI18N
} else if (htmlId == HTMLTokenId.EL_OPEN_DELIMITER) {
//1.check if the next token represents javascript content
String mimetype = (String) ts.token().getProperty("contentMimeType"); //NOT IN AN API, TBD
if (mimetype != null && "text/javascript".equals(mimetype)) {
embeddings.add(snapshot.create("(function(){\n", JsTokenId.JAVASCRIPT_MIME_TYPE)); //NOI18N
//2. check content
if (ts.moveNext()) {
Token<? extends HTMLTokenId> token = ts.token();
if (token.id() == HTMLTokenId.EL_CONTENT) {
//not empty expression: {{sg}}
embeddings.add(snapshot.create(ts.offset(), ts.token().length(), JsTokenId.JAVASCRIPT_MIME_TYPE));
embeddings.add(snapshot.create(";\n});\n", JsTokenId.JAVASCRIPT_MIME_TYPE)); //NOI18N
} else if (token.id() == HTMLTokenId.EL_CLOSE_DELIMITER) {
//empty expression: {{}}
embeddings.add(snapshot.create(ts.offset(), 0, JsTokenId.JAVASCRIPT_MIME_TYPE));
embeddings.add(snapshot.create(";\n});\n", JsTokenId.JAVASCRIPT_MIME_TYPE)); //NOI18N
}
}
}
} else {
// TODO - stash some other DOM stuff into the JavaScript
// file such that I can refer to them from within JavaScript
state.in_javascript = false;
}
}
}
protected static List<EmbeddingPosition> extractJsEmbeddings(String text, int sourceStart) {
List<EmbeddingPosition> embeddings = new LinkedList<>();
// beggining comment around the script
int start = 0;
for (; start < text.length(); start++) {
char c = text.charAt(start);
if (!Character.isWhitespace(c)) {
break;
}
}
if (start < text.length() && text.startsWith("<!--", start)) { //NOI18N
int lineEnd = text.indexOf('\n', start); //NOI18N
if (isHtmlCommentStartToSkip(text, start, lineEnd)) {
if (start > 0) {
embeddings.add(new EmbeddingPosition(sourceStart, start));
}
lineEnd++; //skip the \n
sourceStart += lineEnd;
text = text.substring(lineEnd);
}
}
// inline comments inside script
Scanner scanner = new Scanner(text).useDelimiter("(<!--).*(-->)"); //NOI18N
while (scanner.hasNext()) {
scanner.next();
MatchResult match = scanner.match();
embeddings.add(new EmbeddingPosition(sourceStart + match.start(), match.group().length()));
}
return embeddings;
}
private static boolean isHtmlCommentStartToSkip(String text, int start, int lineEnd) {
if (lineEnd != -1) {
// issue #223883 - one of suggested constructs: http://lachy.id.au/log/2005/05/script-comments (Example 4)
if (text.startsWith("<!--//-->", start)) { //NOI18N
return true;
} else {
// embedded delimiter - issue #217081 || one line comment - issue #223883
return (text.indexOf("-->", start) == -1 || lineEnd < text.indexOf("-->", start)); //NOI18N
}
} else {
return false;
}
}
private static final class JsAnalyzerState {
int inlined_javascript_pieces = 0;
boolean in_javascript = false;
boolean in_inlined_javascript = false;
boolean opening_quotation_stripped = false;
Token<?> lastInlinedJavascriptToken = null;
Embedding lastInlinedJavscriptEmbedding = null;
}
protected static final class EmbeddingPosition {
private final int offset;
private final int length;
public EmbeddingPosition(int offset, int length) {
this.offset = offset;
this.length = length;
}
public int getLength() {
return length;
}
public int getOffset() {
return offset;
}
}
}