blob: c316808ec8459c05a70e5d4333cc878696e53597 [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.html.knockout;
import org.netbeans.modules.html.knockout.api.KODataBindTokenId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.html.lexer.HTMLTokenId;
import static org.netbeans.api.html.lexer.HTMLTokenId.TAG_CLOSE;
import static org.netbeans.api.html.lexer.HTMLTokenId.TAG_OPEN;
import static org.netbeans.api.html.lexer.HTMLTokenId.VALUE;
import org.netbeans.api.lexer.Language;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.html.editor.api.gsf.HtmlParserResult;
import org.netbeans.modules.html.editor.spi.embedding.JsEmbeddingProviderPlugin;
import org.netbeans.modules.html.knockout.KODataBindContext.ParentContext;
import org.netbeans.modules.html.knockout.model.KOModel;
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.spi.knockout.Bindings;
import org.openide.filesystems.FileObject;
import org.openide.util.Pair;
/**
* Knockout javascript virtual source extension
*
* @author mfukala@netbeans.org, phejl@netbeans.org
*/
@MimeRegistration(mimeType = "text/html", service = JsEmbeddingProviderPlugin.class)
public class KOJsEmbeddingProviderPlugin extends JsEmbeddingProviderPlugin {
private static final Logger LOGGER = Logger.getLogger(KOJsEmbeddingProviderPlugin.class.getName());
private static final String WITH_BIND = "with";
private static final String FOREACH_BIND = "foreach";
private static final String TEMPLATE_BIND = "template";
private TokenSequence<HTMLTokenId> tokenSequence;
private Snapshot snapshot;
private List<Embedding> embeddings;
private final Language JS_LANGUAGE;
private final LinkedList<StackItem> stack;
private String lastTagOpen = null;
private final Map<String, KOTemplateContext.TemplateUsage> templateUsages = new HashMap<>();
private final List<TemplateBoundary> templateBoundaries = new LinkedList<>();
private final KODataBindContext dataBindContext = new KODataBindContext();
private final KOTemplateContext templateContext = new KOTemplateContext();
private KODataBindContext currentTemplateContext;
private String generatedSource;
public KOJsEmbeddingProviderPlugin() {
JS_LANGUAGE = Language.find(KOUtils.JAVASCRIPT_MIMETYPE); //NOI18N
this.stack = new LinkedList();
}
@Override
public boolean startProcessing(HtmlParserResult parserResult, Snapshot snapshot, TokenSequence<HTMLTokenId> tokenSequence, List<Embedding> embeddings) {
this.snapshot = snapshot;
this.tokenSequence = tokenSequence;
this.embeddings = embeddings;
if(!KOModel.getModel(parserResult).containsKnockout()) {
return false;
}
FileObject fo = snapshot.getSource().getFileObject();
if (fo != null) {
generatedSource = Bindings.findBindings(fo, 1);
}
return true;
}
@Override
public void endProcessing() {
int offset = 0;
// XXX JsEmbeddingProvider:179 - embeddings are cleared on cancel
// before (!) calling endProcessing
if (!embeddings.isEmpty()) {
for (TemplateBoundary boundary : templateBoundaries) {
if (boundary.isStart()) {
KOTemplateContext.TemplateUsage usage = templateUsages.get(boundary.getName());
if (usage != null) {
KODataBindContext context = usage.getContext();
String name = null;
Set<KOTemplateContext.TemplateUsage> hierarchy = new LinkedHashSet<KOTemplateContext.TemplateUsage>();
hierarchy.add(usage);
while (usage != null && (name = usage.getParentTemplateName()) != null) {
usage = templateUsages.get(name);
// prevents endless loops while evaluating of cycled templates
if (hierarchy.contains(usage)) {
break;
}
hierarchy.add(usage);
if (usage != null) {
context = KODataBindContext.combine(usage.getContext(), context);
}
}
startKnockoutSnippet(context, boundary.getPosition() + offset);
offset++;
} else {
LOGGER.log(Level.FINE, "No usage for template {0}", boundary.getName());
}
} else {
endKnockoutSnippet(boundary.getPosition() + offset);
offset++;
}
}
}
templateUsages.clear();
templateBoundaries.clear();
dataBindContext.clear();
templateContext.clear();
stack.clear();
lastTagOpen = null;
}
@Override
public boolean processToken() {
boolean processed = false;
Pair<Boolean, String> templateCheck = templateContext.process(tokenSequence.token());
if (templateCheck != null) {
if (templateCheck.first()) {
currentTemplateContext = new KODataBindContext();
} else {
currentTemplateContext = null;
}
}
String tokenText = tokenSequence.token().text().toString();
switch (tokenSequence.token().id()) {
case TAG_OPEN:
lastTagOpen = tokenText;
StackItem top = stack.peek();
if (top != null && top.tag.equals(lastTagOpen)) {
top.balance++;
}
break;
case TAG_CLOSE:
top = stack.peek();
if (top != null && top.tag.equals(tokenText)) {
top.balance--;
if (top.balance == 0) {
processed = true;
stack.pop();
String templateId = templateContext.getCurrentScriptId();
if (templateId != null) {
currentTemplateContext.pop();
} else {
dataBindContext.pop();
}
}
}
break;
case VALUE:
TokenSequence<KODataBindTokenId> embedded = tokenSequence.embedded(KODataBindTokenId.language());
boolean setData = false;
boolean setTemplate = false;
if (embedded != null) {
String templateId = templateContext.getCurrentScriptId();
if (templateId != null) {
templateBoundaries.add(new TemplateBoundary(
templateId, embeddings.size(), true));
}
embedded.moveStart();
Token<KODataBindTokenId> dataValue = null;
boolean foreach = false;
while (embedded.moveNext()) {
if (embedded.token().id() == KODataBindTokenId.KEY) {
if (WITH_BIND.equals(embedded.token().text().toString()) // NOI18N
|| FOREACH_BIND.equals(embedded.token().text().toString())) { // NOI18N
stack.push(new StackItem(lastTagOpen));
setData = true;
foreach = FOREACH_BIND.equals(embedded.token().text().toString()); // NOI18N
} else if (TEMPLATE_BIND.equals(embedded.token().text().toString())) {
setTemplate = true;
}
}
if (setData && embedded.token().id() == KODataBindTokenId.VALUE && dataValue == null) {
dataValue = embedded.token();
}
if (setTemplate && embedded.token().id() == KODataBindTokenId.VALUE && dataValue == null) {
KODataBindContext context = currentTemplateContext != null
? currentTemplateContext : dataBindContext;
KODataBindContext templateBindContext = new KODataBindContext(context);
KODataBindDescriptor desc = KODataBindDescriptor.getDataBindDescriptor(
snapshot, embedded.embedded(JsTokenId.javascriptLanguage()), false);
if (desc != null) {
templateBindContext.push(desc.getData(), desc.isIsForEach(), desc.getAlias());
String templateName = desc.getName();
KOTemplateContext.TemplateUsage usage = templateUsages.get(templateName);
if (usage == null) {
usage = new KOTemplateContext.TemplateUsage(templateBindContext);
if (templateId != null) {
usage.addParentTemplateName(templateId);
}
templateUsages.put(templateName, usage);
} else {
KODataBindContext current = usage.getContext();
if (Objects.equals(current.getOriginal(), context)) {
current.setData(current.getData() + " || " + templateBindContext.getData());
} else {
LOGGER.log(Level.INFO, "Multiple incompatible template usage; storing the last one");
usage = new KOTemplateContext.TemplateUsage(templateBindContext);
if (templateId != null) {
usage.addParentTemplateName(templateId);
}
templateUsages.put(templateName, usage);
}
}
} else {
LOGGER.log(Level.INFO, "Cannot get the template name at design time; ignoring");
}
}
if (embedded.embedded(JS_LANGUAGE) != null) {
processed = true;
if (templateId == null) {
startKnockoutSnippet(dataBindContext);
}
String embeddedText = embedded.token().text().toString();
boolean putParenthesis = !embeddedText.trim().isEmpty() &&
!embeddedText.trim().endsWith(";"); // NOI18N
if (putParenthesis) {
embeddings.add(snapshot.create("(", KOUtils.JAVASCRIPT_MIMETYPE)); // NOI18N
}
CharSequence seq = embedded.token().text();
int emptyLength = 0;
for (int i = 0; i < seq.length(); i++) {
if (Character.isWhitespace(seq.charAt(i))) {
emptyLength++;
} else {
break;
}
}
if (emptyLength < seq.length()) {
embeddings.add(snapshot.create(embedded.offset() + emptyLength,
embedded.token().length() - emptyLength, KOUtils.JAVASCRIPT_MIMETYPE));
} else {
embeddings.add(snapshot.create(embedded.offset(),
embedded.token().length(), KOUtils.JAVASCRIPT_MIMETYPE));
}
if (putParenthesis) {
embeddings.add(snapshot.create(")", KOUtils.JAVASCRIPT_MIMETYPE)); // NOI18N
}
if (putParenthesis || !embeddedText.trim().endsWith(";")) { // NOI18N
embeddings.add(snapshot.create(";", KOUtils.JAVASCRIPT_MIMETYPE)); // NOI18N
}
if (templateId == null) {
endKnockoutSnippet();
}
}
}
if (setData) {
if (dataValue != null) {
if (templateId != null) {
currentTemplateContext.push(dataValue.text().toString().trim(), foreach, null);
} else {
KODataBindDescriptor desc = KODataBindDescriptor.getDataBindDescriptor(
snapshot, embedded.embedded(JsTokenId.javascriptLanguage()), true);
if (desc != null) {
dataBindContext.push(desc.getData().trim(), foreach, desc.getAlias());
} else {
dataBindContext.push(dataValue.text().toString().trim(), foreach, null);
}
}
} else {
stack.pop();
}
}
if (templateId != null) {
templateBoundaries.add(new TemplateBoundary(
templateId, embeddings.size(), false));
}
}
break;
default:
break;
}
return processed;
}
private void startKnockoutSnippet(KODataBindContext context) {
startKnockoutSnippet(context, null);
}
private void startKnockoutSnippet(KODataBindContext context, Integer position) {
StringBuilder sb = new StringBuilder();
sb.append("(function(){\n"); // NOI18N
if (generatedSource != null) {
sb.append(generatedSource).append("\n"); //NOI18N
}
// for now this is actually just a placeholder
sb.append("var $element;\n");
// define root as reference
sb.append("var $root = ko.$bindings;\n"); // NOI18N
if (context.isInForEach()) {
sb.append("var $index = 0;\n");
}
// define data object
String currentData = context.getData();
if (currentData == null) {
currentData = "$root"; // NOI18N
}
sb.append("var $parentContext = ");
generateContext(sb, context.getParents());
sb.append(";\n");
sb.append("var $context = ");
List<ParentContext> current = new ArrayList<>(context.getParents());
current.add(new ParentContext(currentData, context.isInForEach(), context.getAlias()));
generateContext(sb, current);
sb.append(";\n");
generateParentAndContextData("$context.", sb, context.getParents());
generateParents(sb, context.getParents());
generateWithHierarchyStart(sb, context.getParents());
String dataValue = currentData;
if ("$root".equals(currentData)) {
dataValue = "ko.$bindings";
}
// may happen if enclosing with/foreach is empty - user is
// going to fill it
if (dataValue.trim().isEmpty()) {
dataValue = "undefined";
}
sb.append("var $data = ").append(dataValue).append(";\n");
if (context.getAlias() != null) {
sb.append("var ").append(context.getAlias()).append(" = ").append(dataValue).append(";\n");
}
generateWithHierarchyEnd(sb, context.getParents());
sb.append("with ($data) {\n");
if (position == null) {
embeddings.add(snapshot.create(sb.toString(), KOUtils.JAVASCRIPT_MIMETYPE));
} else {
embeddings.add(position, snapshot.create(sb.toString(), KOUtils.JAVASCRIPT_MIMETYPE));
}
}
private void endKnockoutSnippet() {
endKnockoutSnippet(null);
}
private void endKnockoutSnippet(Integer position) {
StringBuilder sb = new StringBuilder();
sb.append("}\n");
sb.append("});\n");
if (position == null) {
embeddings.add(snapshot.create(sb.toString(), KOUtils.JAVASCRIPT_MIMETYPE));
} else {
embeddings.add(position, snapshot.create(sb.toString(), KOUtils.JAVASCRIPT_MIMETYPE));
}
}
private static void generateContext(StringBuilder sb, List<ParentContext> parents) {
if (parents.isEmpty()) {
sb.append("undefined");
} else {
sb.append("{\n");
sb.append("$parentContext :");
generateContext(sb, parents.subList(0, parents.size() - 1));
ParentContext parent = parents.get(parents.size() - 1);
sb.append(",\n");
sb.append("$root : ko.$bindings,\n");
if (parent.isInForEach()) {
sb.append("$index : 0,\n");
}
sb.append("}");
}
}
private static void generateParentAndContextData(String additionalPrefix,
StringBuilder sb, List<ParentContext> parents) {
if (parents.isEmpty()) {
if (additionalPrefix != null) {
sb.append(additionalPrefix).append("$parentContext.$data = undefined;\n");
}
sb.append("$parentContext.$data = undefined;\n");
sb.append("var $parent = undefined;\n");
return;
}
StringBuilder prefix = new StringBuilder("$parentContext.");
for (int i = 0; i < parents.size() - 1; i++) {
sb.append("with (").append(parents.get(i).getValue()).append(") {\n");
}
sb.append("var $parent = ").append(parents.get(parents.size() - 1).getValue()).append(";\n");
for (int i = parents.size() - 2; i >= 0; i--) {
if (additionalPrefix != null) {
sb.append(additionalPrefix).append(prefix).append("$data = ").append(parents.get(i + 1).getValue()).append(";\n");
}
sb.append(prefix).append("$data = ").append(parents.get(i + 1).getValue()).append(";\n");
prefix.append("$parentContext.");
sb.append("}\n");
}
if (additionalPrefix != null) {
sb.append(additionalPrefix).append(prefix).append("$data = ko.$bindings;\n");
}
sb.append(prefix).append("$data = ko.$bindings;\n");
}
private static void generateParents(StringBuilder sb, List<ParentContext> parents) {
sb.append("var $parents = ["); // NOI18N
int pos = sb.length();
StringBuilder prefix = new StringBuilder("$parentContext.");
for (int i = 0; i < parents.size(); i++) {
sb.insert(pos, ",");
sb.insert(pos, "$data");
sb.insert(pos, prefix);
prefix.append("$parentContext.");
}
if (!parents.isEmpty()) {
sb.setLength(sb.length() - 1);
}
sb.append("];\n"); // NOI18N
}
private static void generateWithHierarchyStart(StringBuilder sb, List<ParentContext> parents) {
for (ParentContext context : parents) {
if (context.getAlias() != null) {
sb.append("var ").append(context.getAlias()).append(" = ").append(context.getValue()).append(";\n");
}
sb.append("with (").append(context.getValue()).append(") {\n");
}
}
private static void generateWithHierarchyEnd(StringBuilder sb, List<ParentContext> parents) {
for (int i = 0; i < parents.size(); i++) {
sb.append("}\n");
}
}
private static class StackItem {
final String tag;
int balance;
public StackItem(String tag) {
this.tag = tag;
this.balance = 1;
}
}
private static class TemplateBoundary {
private final String name;
private final int position;
private final boolean start;
public TemplateBoundary(String name, int position, boolean start) {
this.name = name;
this.position = position;
this.start = start;
}
public String getName() {
return name;
}
public int getPosition() {
return position;
}
public boolean isStart() {
return start;
}
}
}