summaryrefslogtreecommitdiff
path: root/threads.js
diff options
context:
space:
mode:
Diffstat (limited to 'threads.js')
-rw-r--r--threads.js367
1 files changed, 245 insertions, 122 deletions
diff --git a/threads.js b/threads.js
index cf97362..165ad0e 100644
--- a/threads.js
+++ b/threads.js
@@ -9,7 +9,7 @@
written by Jens Mönig
jens@moenig.org
- Copyright (C) 2014 by Jens Mönig
+ Copyright (C) 2015 by Jens Mönig
This file is part of Snap!.
@@ -83,7 +83,7 @@ ArgLabelMorph, localize, XML_Element, hex_sha512*/
// Global stuff ////////////////////////////////////////////////////////
-modules.threads = '2014-November-17';
+modules.threads = '2015-May-01';
var ThreadManager;
var Process;
@@ -100,8 +100,18 @@ function snapEquals(a, b) {
var x = +a,
y = +b,
+ i,
specials = [true, false, ''];
+ // "zum Schneckengang verdorben, was Adlerflug geworden wäre"
+ // collecting edge-cases that somebody complained about
+ // on Github. Folks, take it easy and keep it fun, okay?
+ // Shit like this is patently ugly and slows Snap down. Tnx!
+ for (i = 9; i <= 13; i += 1) {
+ specials.push(String.fromCharCode(i));
+ }
+ specials.push(String.fromCharCode(160));
+
// check for special values before coercing to numbers
if (isNaN(x) || isNaN(y) ||
[a, b].some(function (any) {return contains(specials, any) ||
@@ -110,7 +120,7 @@ function snapEquals(a, b) {
y = b;
}
- // handle text comparision case-insensitive.
+ // handle text comparison case-insensitive.
if (isString(x) && isString(y)) {
return x.toLowerCase() === y.toLowerCase();
}
@@ -136,7 +146,8 @@ ThreadManager.prototype.toggleProcess = function (block) {
ThreadManager.prototype.startProcess = function (
block,
isThreadSafe,
- exportResult
+ exportResult,
+ callback
) {
var active = this.findProcess(block),
top = block.topBlock(),
@@ -148,9 +159,11 @@ ThreadManager.prototype.startProcess = function (
active.stop();
this.removeTerminatedProcesses();
}
- top.addHighlight();
- newProc = new Process(block.topBlock());
+ newProc = new Process(block.topBlock(), callback);
newProc.exportResult = exportResult;
+ if (!newProc.homeContext.receiver.isClone) {
+ top.addHighlight();
+ }
this.processes.push(newProc);
return newProc;
};
@@ -207,11 +220,10 @@ ThreadManager.prototype.resumeAll = function (stage) {
};
ThreadManager.prototype.step = function () {
-/*
- run each process until it gives up control, skipping processes
- for sprites that are currently picked up, then filter out any
- processes that have been terminated
-*/
+ // run each process until it gives up control, skipping processes
+ // for sprites that are currently picked up, then filter out any
+ // processes that have been terminated
+
this.processes.forEach(function (proc) {
if (!proc.homeContext.receiver.isPickedUp() && !proc.isDead) {
proc.runStep();
@@ -224,9 +236,10 @@ ThreadManager.prototype.removeTerminatedProcesses = function () {
// and un-highlight their scripts
var remaining = [];
this.processes.forEach(function (proc) {
- if (!proc.isRunning() && !proc.errorFlag && !proc.isDead) {
- proc.topBlock.removeHighlight();
-
+ if ((!proc.isRunning() && !proc.errorFlag) || proc.isDead) {
+ if (proc.topBlock instanceof BlockMorph) {
+ proc.topBlock.removeHighlight();
+ }
if (proc.prompter) {
proc.prompter.destroy();
if (proc.homeContext.receiver.stopTalking) {
@@ -235,18 +248,22 @@ ThreadManager.prototype.removeTerminatedProcesses = function () {
}
if (proc.topBlock instanceof ReporterBlockMorph) {
- if (proc.homeContext.inputs[0] instanceof List) {
- proc.topBlock.showBubble(
- new ListWatcherMorph(
- proc.homeContext.inputs[0]
- ),
- proc.exportResult
- );
+ if (proc.onComplete instanceof Function) {
+ proc.onComplete(proc.homeContext.inputs[0]);
} else {
- proc.topBlock.showBubble(
- proc.homeContext.inputs[0],
- proc.exportResult
- );
+ if (proc.homeContext.inputs[0] instanceof List) {
+ proc.topBlock.showBubble(
+ new ListWatcherMorph(
+ proc.homeContext.inputs[0]
+ ),
+ proc.exportResult
+ );
+ } else {
+ proc.topBlock.showBubble(
+ proc.homeContext.inputs[0],
+ proc.exportResult
+ );
+ }
}
}
} else {
@@ -295,9 +312,9 @@ ThreadManager.prototype.findProcess = function (block) {
are children
receiver object (sprite) to which the process applies,
cached from the top block
- context the Context describing the current state
+ context the Context describing the current state
of this process
- homeContext stores information relevant to the whole process,
+ homeContext stores information relevant to the whole process,
i.e. its receiver, result etc.
isPaused boolean indicating whether to pause
readyToYield boolean indicating whether to yield control to
@@ -305,15 +322,20 @@ ThreadManager.prototype.findProcess = function (block) {
readyToTerminate boolean indicating whether the stop method has
been called
isDead boolean indicating a terminated clone process
- timeout msecs after which to force yield
- lastYield msecs when the process last yielded
- errorFlag boolean indicating whether an error was encountered
+ timeout msecs after which to force yield
+ lastYield msecs when the process last yielded
+ errorFlag boolean indicating whether an error was encountered
prompter active instance of StagePrompterMorph
httpRequest active instance of an HttpRequest or null
pauseOffset msecs between the start of an interpolated operation
and when the process was paused
exportResult boolean flag indicating whether a picture of the top
block along with the result bubble shoud be exported
+ onComplete an optional callback function to be executed when
+ the process is done
+ procedureCount number counting procedure call entries,
+ used to tag custom block calls, so "stop block"
+ invocations can catch them
*/
Process.prototype = {};
@@ -321,7 +343,7 @@ Process.prototype.contructor = Process;
Process.prototype.timeout = 500; // msecs after which to force yield
Process.prototype.isCatchingErrors = true;
-function Process(topBlock) {
+function Process(topBlock, onComplete) {
this.topBlock = topBlock || null;
this.readyToYield = false;
@@ -338,6 +360,8 @@ function Process(topBlock) {
this.pauseOffset = null;
this.frameCount = 0;
this.exportResult = false;
+ this.onComplete = onComplete || null;
+ this.procedureCount = 0;
if (topBlock) {
this.homeContext.receiver = topBlock.receiver();
@@ -361,13 +385,13 @@ Process.prototype.isRunning = function () {
// Process entry points
Process.prototype.runStep = function () {
-/*
- a step is an an uninterruptable 'atom', it can consist
- of several contexts, even of several blocks
-*/
+ // a step is an an uninterruptable 'atom', it can consist
+ // of several contexts, even of several blocks
+
if (this.isPaused) { // allow pausing in between atomic steps:
return this.pauseStep();
}
+
this.readyToYield = false;
while (!this.readyToYield
&& this.context
@@ -435,8 +459,10 @@ Process.prototype.pauseStep = function () {
Process.prototype.evaluateContext = function () {
var exp = this.context.expression;
-
this.frameCount += 1;
+ if (this.context.tag === 'exit') {
+ this.expectReport();
+ }
if (exp instanceof Array) {
return this.evaluateSequence(exp);
}
@@ -460,7 +486,7 @@ Process.prototype.evaluateContext = function () {
Process.prototype.evaluateBlock = function (block, argCount) {
// check for special forms
- if (contains(['reportOr', 'reportAnd'], block.selector)) {
+ if (contains(['reportOr', 'reportAnd', 'doReport'], block.selector)) {
return this[block.selector](block);
}
@@ -526,6 +552,35 @@ Process.prototype.reportAnd = function (block) {
}
};
+Process.prototype.doReport = function (block) {
+ var outer = this.context.outerContext;
+ if (this.context.expression.partOfCustomCommand) {
+ this.doStopCustomBlock();
+ this.popContext();
+ } else {
+ while (this.context && this.context.tag !== 'exit') {
+ if (this.context.expression === 'doStopWarping') {
+ this.doStopWarping();
+ } else {
+ this.popContext();
+ }
+ }
+ if (this.context) {
+ if (this.context.expression === 'expectReport') {
+ // pop off inserted top-level exit context
+ this.popContext();
+ } else {
+ // un-tag and preserve original caller
+ this.context.tag = null;
+ }
+ }
+ }
+ // in any case evaluate (and ignore)
+ // the input, because it could be
+ // and HTTP Request for a hardware extension
+ this.pushContext(block.inputs()[0], outer);
+};
+
// Process: Non-Block evaluation
Process.prototype.evaluateMultiSlot = function (multiSlot, argCount) {
@@ -674,9 +729,8 @@ Process.prototype.doYield = function () {
}
};
-Process.prototype.exitReporter = function () {
- // catch-tag for REPORT and STOP BLOCK primitives
- this.popContext();
+Process.prototype.expectReport = function () {
+ this.handleError(new Error("reporter didn't report"));
};
// Process Exception Handling
@@ -757,12 +811,6 @@ Process.prototype.reportJSFunction = function (parmNames, body) {
);
};
-/*
-Process.prototype.doRun = function (context, args, isCustomBlock) {
- return this.evaluate(context, args, true, isCustomBlock);
-};
-*/
-
Process.prototype.doRun = function (context, args) {
return this.evaluate(context, args, true);
};
@@ -787,9 +835,9 @@ Process.prototype.evaluate = function (
}
var outer = new Context(null, null, context.outerContext),
- runnable,
- extra,
+ caller = this.context.parentContext,
exit,
+ runnable,
parms = args.asArray(),
i,
value;
@@ -803,17 +851,11 @@ Process.prototype.evaluate = function (
outer,
context.receiver
);
- extra = new Context(runnable, 'doYield');
-
- // Note: if the context's expression is a ReporterBlockMorph,
- // the extra context gets popped off immediately without taking
- // effect (i.e. it doesn't yield within evaluating a stack of
- // nested reporters)
+ this.context.parentContext = runnable;
if (context.expression instanceof ReporterBlockMorph) {
- this.context.parentContext = extra;
- } else {
- this.context.parentContext = runnable;
+ // auto-"warp" nested reporters
+ this.readyToYield = (Date.now() - this.lastYield > this.timeout);
}
// assign parameters if any were passed
@@ -850,8 +892,9 @@ Process.prototype.evaluate = function (
} else if (context.emptySlots !== 1) {
throw new Error(
- 'expecting ' + context.emptySlots + ' input(s), '
- + 'but getting ' + parms.length
+ localize('expecting') + ' ' + context.emptySlots + ' '
+ + localize('input(s), but getting') + ' '
+ + parms.length
);
}
}
@@ -859,17 +902,22 @@ Process.prototype.evaluate = function (
if (runnable.expression instanceof CommandBlockMorph) {
runnable.expression = runnable.expression.blockSequence();
-
- // insert a reporter exit tag for the
- // CALL SCRIPT primitive variant
if (!isCommand) {
- exit = new Context(
- runnable.parentContext,
- 'exitReporter',
- outer,
- outer.receiver
- );
- runnable.parentContext = exit;
+ if (caller) {
+ // tag caller, so "report" can catch it later
+ caller.tag = 'exit';
+ } else {
+ // top-level context, insert a tagged exit context
+ // which "report" can catch later
+ exit = new Context(
+ runnable.parentContext,
+ 'expectReport',
+ outer,
+ outer.receiver
+ );
+ exit.tag = 'exit';
+ runnable.parentContext = exit;
+ }
}
}
};
@@ -880,6 +928,9 @@ Process.prototype.fork = function (context, args) {
'continuations cannot be forked'
);
}
+ if (!(context instanceof Context)) {
+ throw new Error('expecting a ring but getting ' + context);
+ }
var outer = new Context(null, null, context.outerContext),
runnable = new Context(null,
@@ -926,8 +977,9 @@ Process.prototype.fork = function (context, args) {
} else if (context.emptySlots !== 1) {
throw new Error(
- 'expecting ' + context.emptySlots + ' input(s), '
- + 'but getting ' + parms.length
+ localize('expecting') + ' ' + context.emptySlots + ' '
+ + localize('input(s), but getting') + ' '
+ + parms.length
);
}
}
@@ -944,21 +996,27 @@ Process.prototype.fork = function (context, args) {
stage.threads.processes.push(proc);
};
-Process.prototype.doReport = function (value) {
- if (this.context.expression.partOfCustomCommand) {
- return this.doStopBlock();
+// Process stopping blocks primitives
+
+Process.prototype.doStopBlock = function () {
+ var target = this.context.expression.exitTag;
+ if (isNil(target)) {
+ return this.doStopCustomBlock();
}
- while (this.context && this.context.expression !== 'exitReporter') {
+ while (this.context &&
+ (isNil(this.context.tag) || (this.context.tag > target))) {
if (this.context.expression === 'doStopWarping') {
this.doStopWarping();
} else {
this.popContext();
}
}
- return value;
+ this.pushContext();
};
-Process.prototype.doStopBlock = function () {
+Process.prototype.doStopCustomBlock = function () {
+ // fallback solution for "report" blocks inside
+ // custom command definitions and untagged "stop" blocks
while (this.context && !this.context.isCustomBlock) {
if (this.context.expression === 'doStopWarping') {
this.doStopWarping();
@@ -997,18 +1055,19 @@ Process.prototype.runContinuation = function (aContext, args) {
// Process custom block primitives
Process.prototype.evaluateCustomBlock = function () {
- var context = this.context.expression.definition.body,
+ var caller = this.context.parentContext,
+ context = this.context.expression.definition.body,
declarations = this.context.expression.definition.declarations,
args = new List(this.context.inputs),
parms = args.asArray(),
runnable,
exit,
- extra,
i,
value,
outer;
if (!context) {return null; }
+ this.procedureCount += 1;
outer = new Context();
outer.receiver = this.context.receiver;
outer.variables.parentFrame = outer.receiver ?
@@ -1021,8 +1080,7 @@ Process.prototype.evaluateCustomBlock = function () {
outer.receiver
);
runnable.isCustomBlock = true;
- extra = new Context(runnable, 'doYield');
- this.context.parentContext = extra;
+ this.context.parentContext = runnable;
// passing parameters if any were passed
if (parms.length > 0) {
@@ -1044,24 +1102,43 @@ Process.prototype.evaluateCustomBlock = function () {
}
}
- if (runnable.expression instanceof CommandBlockMorph) {
- // insert a reporter exit tag for the
- // CALL SCRIPT primitive variant
- if (this.context.expression.definition.type !== 'command') {
+ // tag return target
+ if (this.context.expression.definition.type !== 'command') {
+ if (caller) {
+ // tag caller, so "report" can catch it later
+ caller.tag = 'exit';
+ } else {
+ // top-level context, insert a tagged exit context
+ // which "report" can catch later
exit = new Context(
runnable.parentContext,
- 'exitReporter',
+ 'expectReport',
outer,
outer.receiver
);
+ exit.tag = 'exit';
runnable.parentContext = exit;
- } else { // mark all REPORT blocks as being part of a custom command
- runnable.expression.allReportBlocks().forEach(function (rb) {
- rb.partOfCustomCommand = true;
- });
}
- runnable.expression = runnable.expression.blockSequence();
+ // auto-"warp" nested reporters
+ this.readyToYield = (Date.now() - this.lastYield > this.timeout);
+ } else {
+ // tag all "stop this block" blocks with the current
+ // procedureCount as exitTag, and mark all "report" blocks
+ // as being inside a custom command definition
+ runnable.expression.tagExitBlocks(this.procedureCount, true);
+
+ // tag the caller with the current procedure count, so
+ // "stop this block" blocks can catch it, but only
+ // if the caller hasn't been tagged already
+ if (caller && !caller.tag) {
+ caller.tag = this.procedureCount;
+ }
+ // yield commands unless explicitly "warped"
+ if (!this.isAtomic) {
+ this.readyToYield = true;
+ }
}
+ runnable.expression = runnable.expression.blockSequence();
};
// Process variables primitives
@@ -1076,13 +1153,16 @@ Process.prototype.doDeclareVariables = function (varNames) {
Process.prototype.doSetVar = function (varName, value) {
var varFrame = this.context.variables,
name = varName;
-
if (name instanceof Context) {
if (name.expression.selector === 'reportGetVar') {
- name = name.expression.blockSpec;
+ name.variables.setVar(
+ name.expression.blockSpec,
+ value
+ );
+ return;
}
}
- varFrame.setVar(name, value);
+ varFrame.setVar(name, value, this.blockReceiver());
};
Process.prototype.doChangeVar = function (varName, value) {
@@ -1091,10 +1171,14 @@ Process.prototype.doChangeVar = function (varName, value) {
if (name instanceof Context) {
if (name.expression.selector === 'reportGetVar') {
- name = name.expression.blockSpec;
+ name.variables.changeVar(
+ name.expression.blockSpec,
+ value
+ );
+ return;
}
}
- varFrame.changeVar(name, value);
+ varFrame.changeVar(name, value, this.blockReceiver());
};
Process.prototype.reportGetVar = function () {
@@ -1145,7 +1229,7 @@ Process.prototype.doShowVar = function (varName) {
if (isGlobal || target.owner) {
label = name;
} else {
- label = name + ' (temporary)';
+ label = name + ' ' + localize('(temporary)');
}
watcher = new WatcherMorph(
label,
@@ -1246,6 +1330,8 @@ Process.prototype.doDeleteFromList = function (index, list) {
}
if (this.inputOption(index) === 'last') {
idx = list.length();
+ } else if (isNaN(+this.inputOption(index))) {
+ return null;
}
list.remove(idx);
};
@@ -1256,7 +1342,7 @@ Process.prototype.doInsertInList = function (element, index, list) {
return null;
}
if (this.inputOption(index) === 'any') {
- idx = this.reportRandom(1, list.length());
+ idx = this.reportRandom(1, list.length() + 1);
}
if (this.inputOption(index) === 'last') {
idx = list.length() + 1;
@@ -1604,6 +1690,27 @@ Process.prototype.reportMap = function (reporter, list) {
}
};
+Process.prototype.doForEach = function (upvar, list, script) {
+ // perform a script for each element of a list, assigning the
+ // current iteration's element to a variable with the name
+ // specified in the "upvar" parameter, so it can be referenced
+ // within the script. Uses the context's - unused - fourth
+ // element as temporary storage for the current list index
+
+ if (isNil(this.context.inputs[3])) {this.context.inputs[3] = 1; }
+ var index = this.context.inputs[3];
+ this.context.outerContext.variables.addVar(upvar);
+ this.context.outerContext.variables.setVar(
+ upvar,
+ list.at(index)
+ );
+ if (index > list.length()) {return; }
+ this.context.inputs[3] += 1;
+ this.pushContext('doYield');
+ this.pushContext();
+ this.evaluate(script, new List(), true);
+};
+
// Process interpolated primitives
Process.prototype.doWait = function (secs) {
@@ -1709,6 +1816,7 @@ Process.prototype.doAsk = function (data) {
isStage = this.blockReceiver() instanceof StageMorph,
activePrompter;
+ stage.keysPressed = {};
if (!this.prompter) {
activePrompter = detect(
stage.children,
@@ -1828,7 +1936,7 @@ Process.prototype.reportTypeOf = function (thing) {
if (thing === true || (thing === false)) {
return 'Boolean';
}
- if (!isNaN(parseFloat(thing))) {
+ if (!isNaN(+thing)) {
return 'number';
}
if (isString(thing)) {
@@ -2077,6 +2185,9 @@ Process.prototype.reportJoinWords = function (aList) {
// Process string ops
Process.prototype.reportLetter = function (idx, string) {
+ if (string instanceof List) { // catch a common user error
+ return '';
+ }
var i = +(idx || 0),
str = (string || '').toString();
return str[i - 1] || '';
@@ -2107,15 +2218,17 @@ Process.prototype.reportTextSplit = function (string, delimiter) {
str,
del;
if (!contains(types, strType)) {
- throw new Error('expecting a text instad of a ' + strType);
+ throw new Error('expecting text instead of a ' + strType);
}
if (!contains(types, delType)) {
- throw new Error('expecting a text delimiter instad of a ' + delType);
+ throw new Error('expecting a text delimiter instead of a ' + delType);
}
str = (string || '').toString();
switch (this.inputOption(delimiter)) {
case 'line':
- del = '\n';
+ // Unicode Compliant Line Splitting (Platform independent)
+ // http://www.unicode.org/reports/tr18/#Line_Boundaries
+ del = /\r\n|[\n\v\f\r\x85\u2028\u2029]/;
break;
case 'tab':
del = '\t';
@@ -2124,7 +2237,9 @@ Process.prototype.reportTextSplit = function (string, delimiter) {
del = '\r';
break;
case 'whitespace':
- return new List(str.trim().split(/[\t\r\n ]+/));
+ str = str.trim();
+ del = /\s+/;
+ break;
case 'letter':
del = '';
break;
@@ -2289,6 +2404,7 @@ Process.prototype.objectTouchingObject = function (thisObj, name) {
var myself = this,
those,
stage,
+ box,
mouse;
if (this.inputOption(name) === 'mouse-pointer') {
@@ -2300,9 +2416,14 @@ Process.prototype.objectTouchingObject = function (thisObj, name) {
} else {
stage = thisObj.parentThatIsA(StageMorph);
if (stage) {
- if (this.inputOption(name) === 'edge' &&
- !stage.bounds.containsRectangle(thisObj.bounds)) {
- return true;
+ if (this.inputOption(name) === 'edge') {
+ box = thisObj.bounds;
+ if (!thisObj.costume && thisObj.penBounds) {
+ box = thisObj.penBounds.translateBy(thisObj.position());
+ }
+ if (!stage.bounds.containsRectangle(box)) {
+ return true;
+ }
}
if (this.inputOption(name) === 'pen trails' &&
thisObj.isTouching(stage.penTrailsMorph())) {
@@ -2440,6 +2561,7 @@ Process.prototype.reportContextFor = function (context, otherObj) {
if (result.outerContext) {
result.outerContext = copy(result.outerContext);
result.outerContext.receiver = otherObj;
+ result.outerContext.variables.parentFrame = otherObj.variables;
}
return result;
};
@@ -2733,23 +2855,25 @@ Process.prototype.reportFrameCount = function () {
structure:
- parentContext the Context to return to when this one has
+ parentContext the Context to return to when this one has
been evaluated.
outerContext the Context holding my lexical scope
- expression SyntaxElementMorph, an array of blocks to evaluate,
+ expression SyntaxElementMorph, an array of blocks to evaluate,
null or a String denoting a selector, e.g. 'doYield'
receiver the object to which the expression applies, if any
- variables the current VariableFrame, if any
- inputs an array of input values computed so far
+ variables the current VariableFrame, if any
+ inputs an array of input values computed so far
(if expression is a BlockMorph)
- pc the index of the next block to evaluate
+ pc the index of the next block to evaluate
(if expression is an array)
- startTime time when the context was first evaluated
- startValue initial value for interpolated operations
+ startTime time when the context was first evaluated
+ startValue initial value for interpolated operations
activeAudio audio buffer for interpolated operations, don't persist
activeNote audio oscillator for interpolated ops, don't persist
isCustomBlock marker for return ops
- emptySlots caches the number of empty slots for reification
+ emptySlots caches the number of empty slots for reification
+ tag string or number to optionally identify the Context,
+ as a "return" target (for the "stop block" primitive)
*/
function Context(
@@ -2774,6 +2898,7 @@ function Context(
this.activeNote = null;
this.isCustomBlock = false; // marks the end of a custom block's stack
this.emptySlots = 0; // used for block reification
+ this.tag = null; // lexical catch-tag for custom blocks
}
Context.prototype.toString = function () {
@@ -2837,12 +2962,10 @@ Context.prototype.continuation = function () {
} else if (this.parentContext) {
cont = this.parentContext;
} else {
- return new Context(null, 'doStop');
- }
- if (cont.expression === 'exitReporter') {
- return cont.continuation();
+ return new Context(null, 'doYield');
}
cont = cont.copyForContinuation();
+ cont.tag = null;
cont.isContinuation = true;
return cont;
};
@@ -2973,9 +3096,9 @@ VariableFrame.prototype.find = function (name) {
var frame = this.silentFind(name);
if (frame) {return frame; }
throw new Error(
- 'a variable of name \''
+ localize('a variable of name \'')
+ name
- + '\'\ndoes not exist in this context'
+ + localize('\'\ndoes not exist in this context')
);
};
@@ -3040,9 +3163,9 @@ VariableFrame.prototype.getVar = function (name) {
return '';
}
throw new Error(
- 'a variable of name \''
+ localize('a variable of name \'')
+ name
- + '\'\ndoes not exist in this context'
+ + localize('\'\ndoes not exist in this context')
);
};