/*
This file is part of LONCAPA-Daxe.
LONCAPA-Daxe is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
LONCAPA-Daxe is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Daxe. If not, see <http://www.gnu.org/licenses/>.
*/
part of loncapa_daxe;
/**
* Script block display for LON-CAPA
* Jaxe display type: 'script'.
*/
class ScriptBlock extends LCDBlock {
static List<String> perlKeywords = [
'if', 'unless', 'else', 'elsif', 'while', 'until', 'for', 'each', 'foreach', 'next',
'last', 'break', 'continue', 'return', 'my', 'our', 'local', 'state', 'BEGIN', 'END',
'package', 'sub', 'do', 'given ', 'when ', 'default', '__END__', '__DATA__',
'__FILE__', '__LINE__', '__PACKAGE__'
];
static List<String> javascriptKeywords = [
'abstract', 'arguments', 'boolean', 'break', 'byte',
'case', 'catch', 'char', 'class*', 'const',
'continue', 'debugger', 'default', 'delete', 'do',
'double', 'else', 'enum*', 'eval', 'export*',
'extends*', 'false', 'final', 'finally', 'float',
'for', 'function', 'goto', 'if', 'implements',
'import*', 'in', 'instanceof', 'int', 'interface',
'let', 'long', 'native', 'new', 'null',
'package', 'private', 'protected', 'public', 'return',
'short', 'static', 'super*', 'switch', 'synchronized',
'this', 'throw', 'throws', 'transient', 'true',
'try', 'typeof', 'var', 'void', 'volatile',
'while', 'with', 'yield'
];
String currentType = null;
ScriptBlock.fromRef(x.Element elementRef) : super.fromRef(elementRef) {
}
ScriptBlock.fromNode(x.Node node, DaxeNode parent) : super.fromNode(node, parent) {
if (firstChild is DNText && firstChild.nextSibling == null) {
String text = firstChild.nodeValue;
// remove one newline after <![CDATA[ and before ]]>
if (text.startsWith('\n'))
text = text.substring(1);
if (text.endsWith('\n'))
text = text.substring(0, text.length - 1);
firstChild.replaceWith(new ScriptText(text));
}
}
@override
h.Element html() {
h.Element div = super.html();
if (firstChild == null || firstChild.nextSibling != null || firstChild is! ScriptText)
return div;
if (state != 2 && hasContent) {
h.DivElement contents = div.lastChild;
String type = _getTypeValue();
currentType = type;
if (type == 'loncapa/perl' || type == 'text/javascript') {
contents.classes.add('script-text');
contents.style.position = 'relative';
h.DivElement overlay;
if (type == 'loncapa/perl')
overlay = createPerlOverlay();
else
overlay = createJavascriptOverlay();
contents.append(overlay);
} else {
contents.style.fontFamily = 'monospace';
}
}
return div;
}
String _getTypeValue() {
String type = getAttribute('type');
if (type == null) {
// check if there is a default value
x.Element attRef = doc.cfg.attributeReference(ref, 'type', null);
type = doc.cfg.defaultAttributeValue(attRef);
}
return type;
}
void addLineNumber(div, sb, line_number) {
if (sb.length > 0) {
div.append(new h.Text(sb.toString()));
sb.clear();
}
h.SpanElement span1 = new h.SpanElement();
span1.classes.add('script-line-number-position');
h.SpanElement span2 = new h.SpanElement();
span2.classes.add('script-line_number');
String s = line_number.toString();
span2.appendText(s);
span1.append(span2);
div.append(span1);
}
h.Element createPerlOverlay() {
// NOTE: this is very basic, we might need a real parser to do something more complex
// TODO: add special highlighting for string redirect << and regular expressions
final String letters = '\$@%&abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'; // starting chars in names
final String digits = '0123456789';
h.DivElement div = new h.DivElement();
div.classes.add('script-colored');
String text = firstChild.nodeValue;
StringBuffer sb = new StringBuffer();
bool in_name = false;
bool in_comment = false;
bool in_number = false;
bool in_string = false;
bool in_backslash = false;
String string_start;
int i = 0;
int line_number = 1;
if (text.length > 0) {
addLineNumber(div, sb, line_number);
line_number++;
}
while (i < text.length) {
String c = text[i];
if (!in_string && !in_comment && (
(letters.contains(c) && !((c == 'e' || c == 'E') && in_number)) ||
(in_name && digits.contains(c)) ||
(in_name && c == '#' && sb.toString()[sb.length-1] == '\$'))) {
if (!in_name ) {
if (sb.length > 0) {
div.append(new h.Text(sb.toString()));
sb.clear();
}
in_name = true;
}
} else if (!in_string && !in_name && !in_comment && (digits.contains(c) ||
((c == '.' || c == 'e' || c == 'E') && in_number))) {
if (!in_number) {
if (sb.length > 0) {
div.append(new h.Text(sb.toString()));
sb.clear();
}
in_number = true;
}
} else {
if (in_name) {
String s = sb.toString();
h.SpanElement span = new h.SpanElement();
if (perlKeywords.contains(s))
span.classes.add('keyword');
else if (s.startsWith('\$') || s.startsWith('@') || s.startsWith('%'))
span.classes.add('variable');
else if (s.startsWith('&') || c == '(')
span.classes.add('function-call');
else
span.classes.add('name');
span.appendText(s);
div.append(span);
sb.clear();
in_name = false;
} else if (in_number) {
h.SpanElement span = new h.SpanElement();
span.classes.add('number');
span.appendText(sb.toString());
div.append(span);
sb.clear();
in_number = false;
} else if (in_comment && (c == '\n' || c == '\r')) {
h.SpanElement span = new h.SpanElement();
span.classes.add('comment');
span.appendText(sb.toString());
div.append(span);
sb.clear();
in_comment = false;
}
if (!in_comment && (c == '"' || c == "'") && !in_backslash) {
if (in_string) {
if (c == string_start) {
h.SpanElement span = new h.SpanElement();
span.classes.add('string');
span.appendText(sb.toString());
div.append(span);
sb.clear();
in_string = false;
}
} else {
if (sb.length > 0) {
div.append(new h.Text(sb.toString()));
sb.clear();
}
string_start = c;
in_string = true;
}
} else if (!in_string && c == '#' && !in_comment) {
if (sb.length > 0) {
div.append(new h.Text(sb.toString()));
sb.clear();
}
in_comment = true;
}
}
if (in_string) {
if (c == '\\')
in_backslash = !in_backslash;
else if (in_backslash)
in_backslash = false;
if (c == '\n') {
// multiline string, need to split it because of line numbers
h.SpanElement span = new h.SpanElement();
span.classes.add('string');
span.appendText(sb.toString());
div.append(span);
sb.clear();
}
}
sb.write(c);
if (c == '\n') {
addLineNumber(div, sb, line_number);
line_number++;
}
i++;
}
if (sb.length > 0) {
if (in_comment) {
h.SpanElement span = new h.SpanElement();
span.classes.add('comment');
span.appendText(sb.toString());
div.append(span);
} else
div.append(new h.Text(sb.toString()));
}
return(div);
}
h.Element createJavascriptOverlay() {
// NOTE: this is very basic, we might need a real parser to do something more complex
final String letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'; // starting chars in names
final String digits = '0123456789';
h.DivElement div = new h.DivElement();
div.classes.add('script-colored');
String text = firstChild.nodeValue;
StringBuffer sb = new StringBuffer();
bool in_name = false;
bool in_comment = false;
bool in_oneline_comment = false;
bool in_multiline_comment = false;
bool in_number = false;
bool in_string = false;
bool in_backslash = false;
String c2; // last character in sb, to avoid calling sb.toString() in the tests
String string_start;
int i = 0;
int line_number = 1;
if (text.length > 0) {
addLineNumber(div, sb, line_number);
line_number++;
}
while (i < text.length) {
String c = text[i];
if (!in_string && !in_comment && (
(letters.contains(c) && !((c == 'e' || c == 'E') && in_number)) ||
(in_name && digits.contains(c)))) {
if (!in_name ) {
if (sb.length > 0) {
div.append(new h.Text(sb.toString()));
sb.clear();
c2 = '';
}
in_name = true;
}
} else if (!in_string && !in_name && !in_comment &&
(digits.contains(c) || ((c == '.' || c == 'e' || c == 'E') && in_number))) {
if (!in_number) {
if (sb.length > 0) {
div.append(new h.Text(sb.toString()));
sb.clear();
c2 = '';
}
in_number = true;
}
} else {
if (in_name) {
String s = sb.toString();
h.SpanElement span = new h.SpanElement();
if (javascriptKeywords.contains(s))
span.classes.add('keyword');
else if (c == '(')
span.classes.add('function-call');
else
span.classes.add('name');
span.appendText(s);
div.append(span);
sb.clear();
c2 = '';
in_name = false;
} else if (in_number) {
h.SpanElement span = new h.SpanElement();
span.classes.add('number');
span.appendText(sb.toString());
div.append(span);
sb.clear();
c2 = '';
in_number = false;
} else if (in_oneline_comment && (c == '\n' || c == '\r')) {
h.SpanElement span = new h.SpanElement();
span.classes.add('comment');
span.appendText(sb.toString());
div.append(span);
sb.clear();
c2 = '';
in_comment = false;
in_oneline_comment = false;
}
if (!in_comment && (c == '"' || c == "'") && !in_backslash) {
// strings
if (in_string) {
if (c == string_start) {
h.SpanElement span = new h.SpanElement();
span.classes.add('string');
span.appendText(sb.toString());
div.append(span);
sb.clear();
c2 = '';
in_string = false;
}
} else {
if (sb.length > 0) {
div.append(new h.Text(sb.toString()));
sb.clear();
c2 = '';
}
string_start = c;
in_string = true;
}
} else if (!in_string && (c == '/' || c == '*')) {
// comments
if (!in_comment) {
if (c2 == '/') {
String s = sb.toString();
div.append(new h.Text(s.substring(0, s.length - 1)));
sb.clear();
sb.write(c2);
in_comment = true;
if (c == '/')
in_oneline_comment = true;
else if (c == '*')
in_multiline_comment = true;
}
} else if (in_multiline_comment) {
if (c2 == '*' && c == '/') {
h.SpanElement span = new h.SpanElement();
span.classes.add('comment');
sb.write(c);
span.appendText(sb.toString());
div.append(span);
sb.clear();
c2 = '';
in_comment = false;
in_multiline_comment = false;
i++;
continue;
}
}
} else if (in_multiline_comment && c == '\n') {
// multiline comment, need to split it because of line numbers
h.SpanElement span = new h.SpanElement();
span.classes.add('comment');
span.appendText(sb.toString());
div.append(span);
sb.clear();
}
}
if (in_string) {
if (c == '\\')
in_backslash = !in_backslash;
else if (in_backslash)
in_backslash = false;
}
sb.write(c);
if (c == '\n') {
addLineNumber(div, sb, line_number);
line_number++;
}
c2 = c;
i++;
}
if (sb.length > 0) {
if (in_comment) {
h.SpanElement span = new h.SpanElement();
span.classes.add('comment');
span.appendText(sb.toString());
div.append(span);
} else
div.append(new h.Text(sb.toString()));
}
return(div);
}
@override
void updateHTMLAfterChildrenChange(List<DaxeNode> changed) {
updateHTML();
}
bool get needsSpecialDNText {
return true;
}
@override
DNText specialDNTextConstructor(String text) {
return new ScriptText(text);
}
/**
* Speed optimization: instead of calling updateHTML() for
* each inserted character, only the block contents are updated.
*/
void textUpdate() {
h.DivElement div = getHTMLNode();
if (state != 2 && hasContent && div != null) {
h.DivElement contents = div.lastChild;
String type = _getTypeValue();
if (type == currentType) {
if (type == 'loncapa/perl' || type == 'text/javascript') {
h.DivElement overlay;
if (type == 'loncapa/perl')
overlay = createPerlOverlay();
else
overlay = createJavascriptOverlay();
h.SpanElement rawText = contents.firstChild;
rawText.text = firstChild.nodeValue;
// FIXME: this is slow for long scripts, we should split the lines
// in html() and change only the relevant lines here
// (but scripts are supposed to be short in LC problems).
h.DivElement oldOverlay = contents.lastChild;
oldOverlay.replaceWith(overlay);
return;
}
}
}
super.updateHTML();
}
@override
void updateAttributes() {
// this is useful for undo/redo
if (state == 2)
return;
String type = _getTypeValue();
if (currentType != type) {
updateHTML();
} else {
super.updateAttributes();
}
}
@override
void changeAttributeValue(x.Element refAttr, SimpleTypeControl attributeControl) {
// LCDBlock uses updateDisplay:false, so we need to override this
// to update the display when the type attribute is changed.
// Full element updates are limited to relevant attribute changes.
super.changeAttributeValue(refAttr, attributeControl);
if (doc.cfg.attributeName(refAttr) == 'type') {
if (state == 2)
return;
String type = _getTypeValue();
if (currentType != type) {
if (type == 'loncapa/perl' || type == 'text/javascript' ||
currentType == 'loncapa/perl' || currentType == 'text/javascript') {
updateHTML();
attributeControls['type'].focus();
}
}
}
}
/**
* Override to use a CDATA section.
*/
@override
x.Node toDOMNode(x.Document domDocument) {
if (firstChild == null || firstChild is! DNText || firstChild.nextSibling != null)
return super.toDOMNode(domDocument);
String text = '\n' + firstChild.nodeValue + '\n';
x.Element el = domDocument.createElementNS(namespaceURI, nodeName);
for (DaxeAttr att in attributes)
el.setAttributeNS(att.namespaceURI, att.name, att.value);
x.CDATASection cdata = domDocument.createCDATASection(text);
el.appendChild(cdata);
return(el);
}
}
/**
* DNText that calls parent.textUpdate() instead of updateHTML().
*/
class ScriptText extends DNText {
ScriptText(String s) : super(s);
ScriptText.fromNode(x.Node node, DaxeNode parent) : super.fromNode(node, parent);
@override
void updateHTML() {
(parent as ScriptBlock).textUpdate();
}
}
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>