Actions

ExpressionScript for developers: Difference between revisions

From LimeSurvey Manual

No edit summary
 
mNo edit summary
 
(22 intermediate revisions by 5 users not shown)
Line 2: Line 2:
__TOC__
__TOC__


This wiki page is meant for the LimeSurvey development team and others wishing to contribute to LimeSurvey.  It provides details about how to work with, test, and extend Expression Manager (EM).
{{Alert|text=Starting 2020, we renamed the Expression Manager to ExpressionScript.}}
 
This wiki page is meant for the LimeSurvey development team and others wishing to contribute to LimeSurvey. It provides details about how to work with, test, and extend ExpressionScript (ES).
 
 
{{Alert|text=Please note that his document is currently incomplete.}}
 
 
==Getting Started==


=Getting Started=


The best way to get started with EM is to:
The best way to get started with EM is to:
*Install the LimeSurvey_CI branch
*Install [https://www.limesurvey.org/about-limesurvey/download the latest version of LimeSurvey]
**[[Accessing the source code|Download and install the code]] from [https://limesurvey.svn.sourceforge.net/svnroot/limesurvey/source/limesurvey_ci this URL].
*Load and play with the ExpressionScript demos (located in /docs/demosurveys of the distribution)
**Run the install script:  http://localhost/limesurvey_ci/index.php/installer
*Navigate through all of the test cases
*Navigate through all of the test cases:  http://localhost/limesurvey_ci/index.php/admin/expressions/test
**To see the ExpressionScript displayed in the configuration toolbar, you must activate first the debug mode from application/config/config.php.
**Note, you will have to login first
**Try adding tests to any of the test cases suites. Each test is one line of code, so this is very easy.
*Load and play with the Expression Manager demo(s)
**[http://www.limesurvey.org/en/additional-downloads/Surveys/ExpressionManager-Demo/ ExpressionManager-Demo survey]
**Extend the demos
*Read the documentation
*Read the documentation
**Including the user documentation for [[Expression Manager|Expression Manager]]
**Main user documentation for [[ExpressionScript - quick start guide|ExpressionScript]]
**Add to the EM documentation, or contact TMSWhite, if it is unclear.
**ExpressionScript [[ExpressionScript HowTos|How Tos]]
 
 
==EM Source Code Organization and Purpose==
 
 
=== ExpressionScript Engine (EM) ===
 
 
Implements a [https://en.wikipedia.org/wiki/Recursive_descent_parser recursive descent parser] in PHP and JavaScript which let you create variables and securely expose a specified set of pre-defined functions.
 
 
==== Location ====
 
 
* Version 1.92:  /classes/expressions/ExpressionScript Engine.php
* Versions 2.0 and 3.0:  /application/helpers/expressions/em_core_helper.php
 
 
==== Recursive Descent Parser (all variables and functions starting with ''RDP_'') ====
 
 
* This is the core of the EM, a compiler which builds and evaluates the parse tree for the expression
* Unless you are an expert in compiler theory, you should not touch the RDP_ code.
 
 
==== API Functions to the RDP ====
 
 
* RegisterFunctions - an interface to add external functions to EM
* sProcessStringContainingExpressions() - splits string on expressions, evaluates each, then joins together the parts
* ProcessBooleanExpression() - evaluates whether an equation (not surrounded by curly braces) is true
* Get*() - returns information about the most recently processed expressions
 
 
=== LimeExpressionScript Engine (LEM) ===
 
 
This centralizes the integration of LimeSurvey with EM.
 
 
==== Location ====
 
 
* Version 1.92:  /classes/expressions/LimeExpressionScript Engine.php
* Versions 2.0 and 3.0:  /application/helpers/expressions/em_manager_helper.php
 
 
==== Navigation Functions ====
 
 
These process the current responses, update the database, find the next relevant set of questions, and create the metadata needed to render those questions:
* NavigateForwards()
* NavigateBackwards()
* JumpTo()
* GetLastMoveResult()
 
 
==== Initialization Functions ====
 
 
* StartSurvey() - initializes the survey, setting all variable mappings
* setVariableAndTokenMappingsForExpressionScript Engine() - post-processes CreateFieldMap() to create metadata needed for all variables and token values
* _CreateSubQLevelRelevanceAndValidationEquations() - processes advanced question attributes such as array_filter, min_answers, and min_value
* ProcessAllNeededRelevance() - computes the relevance results (and JavaScript) for all applicable questions
* _ProcessGroupRelevance() - computes the relevance of a group based upon its own relevance criteria and the status of the questions it contains.
 
 
==== Functions to Generate Tailored Content ====
 
 
* StartProcessingPage() - called at beginning of each page
* StartProcessingGroup() - called for each group on a page
* FinishProcessingGroup() - collects all tailoring for this group so can render appropriate JavaScript
* FinishProcessingPage() - completes the EM-related processing, and serializes LEM into $_SESSION<nowiki>[</nowiki>'LEMsingleton']
* GetRelevanceAndTailoringJavaScript() - collects all relevance, validation, and tailoring logic and generates ExprMgr_process_relevance_and_tailoring() JavaScript function
 
 
==== Response Management ====
 
 
*ProcessCurrentResponses() - validates and processes the $_POSTed responses
*_UpdateValuesInDatabase() - saves values to database, including NULLing irrelevant values
 
 
==== Caching ====
 
 
LEM tries to avoid calling the Initialization functions each page, storing state in $_SESSION<nowiki>[</nowiki>'LEMsingleton']
* SetDirtyFlag() - when called, tells LEM to force a rebuild of the Initialization functions (e.g. of admin has added/removed/updated any questions, groups, conditions, or defaults)
* SetSurveyId()- calls SetDirtyFlag if the user wants to work with a different survey from the one that is cached
* SetEMLanguage() - calls SetDirtyFlag() if the user starts  to use a language different from the cached one
 
 
==== Validation Functions ====
 
 
* _ValidateSurvey() - validates the entire survey by calling _ValidateGroup()
* _ValidatGroup() - validates a group and returns its status by calling _ValidateQuestion() on all of its questions
* _ValidateQuestion() - validates a question, determining whether it is relevant, hidden, or fails any validation and/or mandatory criteria
 
 
===EM-related JavaScript===
 
 
*Location
**Version 1.92: /classes/expressions/em_javascript.js
**Versions 2.0 and 3.0: /scripts/admin/expressions/em_javascript.js
*Purpose - contains all of the custom javascript needed for EM
 
 
====JavaScript equivalents of PHP functions====
 
 
About 20 functions from phpjs.org
 
 
====Core Functions====
 
 
*LEManyNA() - checks whether any of the variables are irrelevant
*LEMval() - retrieves the value for any variable, or its metadata (via the dot notation syntax)
*LEMsetTabIndexes() - ensures that the tab sequence will act as expected even if input elements change visibility.
 
 
===EM-related Test Cases===
 
 
*Location
**Version 1.92: /classes/expressions/test/*.php
**Versions 2.0 and 3.0:  /application/views/admin/expressions/*
*Purpose
**'''Available Functions''' - runs EM::ShowAllowableFunctions to display functions and syntax from EM->RDP_ValidFunctions
**'''String Splitter''' - runs EM::UnitTestStringSplitter() to show how it parses strings with curly braces
**'''Tokenizer''' - runs EM::UnitTestTokenizer() to show how EM detects and categorizes tokens (e.g. variables, string, functions, operators)
**'''Unit Tests of Isolated Expressions''' - runs EM::UnitTestEvaluator() for unit tests of each of ExpressionScript's features (e.g. all operators and functions). Color coding shows whether any tests fail. Syntax highlighting shows cases where ExpressionScript properly detects bad syntax.
**'''Unit Tests of Expressions Within Strings''' - runs LEM::UnitTestProcessStringContainingExpressions() to show how ExpressionScript can process strings containing one or more variable, token, or expression replacements surrounded by curly braces.
**'''Unit Test Dynamic Relevance Processing''' - runs LEM::UnitTestRelevance() to show how questions and substitutions should dynamically change based upon values entered.
**'''Preview Conversion of Conditions to Relevance''' - runs LEM::UnitTestConvertConditionsToRelevance() to show relevance equations for all conditions in the database, grouped by question id
**'''Bulk Convert Conditions to Relevance''' - actually performs the conversion, saving the generated relevance  equations (while retaining the original conditions)
**'''Test Navigation''' - unit tests LEM::NavigateForwards() for the selected survey, generating the following information based upon the selected debugging options:
***Detailed Timing - shows low-level timing information for each part of EM (e.g. to examine duration of database calls)
***Validation Summary - shows one line per group and question, showing its relevance equation, and indicating whether it was irrelevant, hidden, mandatory and/or failed validation criteria.  Also shows the generated SQL per navigation step to update the database
***Validation Detail - shows extra details per question, including
****Validation Tip, Equation, JavaScript equivalent of the Equation
****Lists of subquestions; which are relevant; and which are unanswered
****List of array filters applied, by subquestion
***Pretty Print Syntax - syntax highlights all of the equations so that you can see errors, and also click on variable names to jump to those questions and edit them.
**'''Show Survey Logic File''' - generates the logic file which is available via the "QA" buttons in the admin console.
 
 
==How EM Works==
 
 
===What is an Expression?===
 
 
Anything surrounded by curly braces is an Expression, with two exceptions:
*if there is whitespace after the opening brace or before the closing brace, it is ignored.
**The EM can ignore in this way the embedded JavaScript.
**So, if you have JavaScript that might be parsed by EM, make sure to add a space or newline after the opening brace.
*Escaped curly braces are ignored (e.g. <span style='color:#F00'>'''\{'''</span> and <span style='color:#F00'>'''\}'''</span>)
 
Note that EM does support Expressions within strings. Moreover, Expressions can contain nested strings, but not nested expressions. So, the following red sections are valid Expressions and will cause substitions to occur within the containing strings.
*<img src="images/mine_<span style='color:#F00'>'''{Q1}'''</span>.png">
*<img src="images/mine_<span style='color:#F00'>'''{if(Q1=="Y",'yes','no')}'''</span>.png">
*<img src="images/mine_<span style='color:#F00'>'''{if(Q1=="Y",'single quote with {nested braces}',"double quote with {nested braces}")}'''</span>.png">
 
 
===What does EM do with text containing expressions?===
 
 
#A regular expression divides the source line into STRING and EXPRESSION tokens
#Each EXPRESSION is parsed by ExpressionScript Engine, a [http://en.wikipedia.org/wiki/Recursive_descent_parser recursive descent parser].
##If there are syntax errors, EM returns an HTML string that syntax-highlights the equation and puts red-lined boxes around syntax errors
##If there are no syntax errors, EM returns the result of evaluating the expression
#EM re-joins the STRING and EM-evaluated EXPRESSION parts.
#EM optionally appends the translation activity to structures used by GetRelevanceAndTailoringJavaScript().
 
 
===How can we be sure that EM accurately parses the equations?===
 
 
EM was originally written in 1999-2000 by Dr. Tom White (TMSWhite) for a different project (Dialogix) in Java, using [http://en.wikipedia.org/wiki/JavaCC JavaCC], an open source compiler compiler (parser generator).  That Java-based project has been in production for over a decade, and has been fully vetted for unit and integration tests.
 
Since there is no production-grade parser generator for PHP and JavaScript (although [http://www.antlr.org/ Antlr] is coming close), TMSWhite created a custom [http://en.wikipedia.org/wiki/Recursive_descent_parser recursive descent parser] for LimeSurvey.  To ensure its accuracy, EM's logic is based upon the JavaCC source code for Dialogix.  The JavaCC syntax mirrors the functionality needed for a recursive descent parser.  JavaCC happens to build a state-based compiler, which is a little more efficient than a recusive descent parser.  However, state-based compilers are impossible to read, understand,  or expand without JavaCC-like source code, so it did not make sense to try to port the JavaCC output directly to PHP.
 
Futhermore, there are comprehensive unit and integration test suites for EM. These make it easy to validate the accuracy of the EM system.  Each test suite includes dozens to hundreds of test cases, and it is trivial to add addition test cases.
 
 
===How does the Recursive Descent Parser work?===
 
 
EM must do the following:
#Tokenize the expression - separating strings, words (variable names vs. functions), and punctuation; and categorizing the types of each.
#Analyze the tokens to build a parse tree, checking for syntax errors along the way.
#Return the result of evaluating the expression (using PHP) (or return syntax-highlighted HTML if there are syntax errors).
#Create a safe JavaScript equivalent of the expression so that expressions can be  dynamically re-computed client-side.
#Determine which variables are used in each expression (so can make sure they are available client-side).
 
 
===How does EM integrate into LimeSurvey?===
 
 
The LimeExpressionScript Engine (LEM) class manages the integration of EM into LimeSurvey.  LEM must:
#Initialize all of the variables needed by LimeSurvey (e.g., for TOKENS, INSERTANS, and templatereplace())
#Know which Group and Question are being processed
#Record the results and metadata about all of the text that LimeSurvey asks it to process
#Output static HTML that reflects the results of that processing
#Output JavaScript that lets those results be dynamically re-computed if values on the page change.
 
 
==Extending EM==
 
 
=== Adding Functions ===
 
 
When you add a function that does not exist in PHP, add it also to the body of '''em_core_helper.php'''
 
''FIXME'':  Eventually we should separate out such add-on functions into their own php file.
 
Functions are stored in LimeExpressionScript Engine::amValidFunctions[]. Some existing examples are:
<syntaxhighlight lang=PHP>
'abs' => array('abs', 'Math.abs', 'Absolute value', 'number abs(number)', 'http://www.php.net/manual/en/function.checkdate.php', 1),
'if' => array('exprmgr_if', 'LEMif', 'Excel-style if(test,result_if_true,result_if_false)', 'if(test,result_if_true,result_if_false)', '', 3),
'max' => array('max', 'Math.max', 'Find highest value', 'number max(arg1, arg2, ... argN)', 'http://www.php.net/manual/en/function.max.php', -2),
'substr' => array('substr', 'substr', 'Return part of a string', 'string substr(string, start <nowiki>[</nowiki>, length])', 'http://www.php.net/manual/en/function.substr.php', 2,3)
</syntaxhighlight>
 
The syntax for each function is:
<syntaxhighlight lang=PHP>
function => array( detail_1, detail_2, detail_3, detail_4, detail_5, detail_6 )
</syntaxhighlight>
 
The ''details'' which must be included in the array are:
 
# PHP function name - this is the PHP function that will be called for that func.
# JavaScript function name - this is the JavaScript function that will be called for that function
# Meaning - this is a short description of what the function does
# Syntax - this shows the valid syntax for the function
# Reference - this is an optional URL showing more details about the syntax (e.g. a link to the PHP documentation)
# Number of required arguments
#* Multiple values are allowed at the end of the array. For example, substr() above can take 2 or 3 arguments
#* Negative values mean that the function accepts a variable number of arguments
#* Negative values less than -1 mean that the function requires at least abs(N)-1 arguments (so -2 means it requires at least 1 argument)
 
If you add a function that does not exist in JavaScript, then add it to the body of '''em_javascript.js'''
 
 
===Adding Test Cases===
 
 
Please do! The testing frameworks are solid. You just need to add more tests following the examples in the code.
 
Make sure to add Unit tests for new functions to UnitTestEvaluator
 
* Syntax is ExpectedResult~Expression, such as:
* 212~5 + max(1,(2+3),(4 + (5 + 6)),[[]],[[7 + 8) + 9),( (10 + 11), 12),(13 + (14 * 15) - 16) )
 
When your test case should return an error, use NULL as the expected value, like this:


=EM Source Code Organization and  Purpose=
* NULL~four * / seven
* NULL~(5 + 7) = 8


EM includes the following source fies:
[[Category:Development]]
#/application/helpers/admin/expressions/'''em_core_helper.php'''
**ExpressionManager class - which implements a [http://en.wikipedia.org/wiki/Recursive_descent_parser recursive descent parser] in PHP and JavaScript which let you create variables and securely expose a specified set of pre-defined functions.
**Defines the set of available functions within EM ($this->amValidFunctions)
**exprmgr_*() functions - custom PHP functions exposed to the user via EM.
**Has built-in test cases:
***UnitTestStringSplitter() - validates that properly extracts expressions (surrounded by curly braces) from longer strings
***UnitTestTokenizer() - validates that strings are properly tokenized assigned the right token classification
***ShowAllowableFunctions() - shows a table of the 70+ avaiable functions and their syntax
***UnitTestEvaluator() - this should test all EM syntax, operators, and functions in PHP and JavaScript and confirm they generate accurate and identical results.  Currently, it contains 200+ test cases, but does not yet test every function.
#/application/helpers/admin/expressions/'''em_manager_helper.php'''
**LimeExpressionManager class - this implements LimeSurvey-specific functions for accessing ExpressionManager. Initially, the goal was to keep the ExpressionManager completely separate  (e.g. de-coupled)from LimeSurvey so that developers would never need to modify the ExpressionManager class itself.  However, there is now some tight coupling between LimeExpressionManager and ExpressionManager
**It is implemented as a singleton, so all access is via static functions.  Theoretically, this could be loaded as a library within CodeIgniter, but it isn't clear that that provides additional value, and would require significant re-write (since ExpressionManager was initially designed for LimeSurvey 1.91+)
**Has built-in test cases:
***UnitTestProcessStringContainingExpressions() - validates that accurately evaluates multiple expressions within a string
***UnitTestRelevance() - implements a 16+ question survey with cascading relevance, making questions and tailoring appaer and disappear based upon answers provided.
***Lacks a full integration test case - for that, one needs to load and test the [http://www.limesurvey.org/en/additional-downloads/Surveys/ExpressionManager-Demo/ ExpressionManager-Demo survey].
#/application/views/admin/expressions/'''test.php'''
**Provides access to each of the Test cases, whose views are in /application/views/admin/expressions/test/*.php
***Available Functions - shows the 70+ functions people can use via EM and their allowable syntax
***Tokenizer - calls UnitTestTokenizer()
***Unit Tests- calls UnitTestEvaluator() to test core EM functionality.  Cells in Green are correct.  Cells in Red are errors (except for dynamic functions, like rand() and date().
***String Splitter - calls UnitTestStringSplitter()
***Integration Tests - calls UnitTestProcessStringContainingExpressions()
***Unit Test Dynamic Relevance Processing - calls UnitTestRelevance()
***Running Log - Source Data - shows a color-coded dump of the current instrument definition (e.g. $fieldmap[])
***Running Log - Transactions on this Page - shows all of the EM-related translation requests on the current survey page (pretty-printed), and their results
#/scripts/admin/expressions/'''em_javascript.js'''
**LEMval() - provides access to internal LimeSurvey variable values and attributes.  Allowable attributes are:
***.shown - the answer as displayed to the user
***.qid - the question ID
***.mandatory - whether the question is mandatory
***.question - the text of the question
***.relevance - the relevance equation for the question
***.relevanceStatus - whether or not the question is currently relevant
***.type - the question type (the one character code)
***.code (or no suffix) - the internal code value for the answer
***.NAOK - the internal code value for the answer
**Fix for Tab-based navigation
***Purpose:  browsers do now properly handle tabs if form elements appear or disappear.  Without this fix, if a change to one question makes another question appear immediately after it, the built-in tab functionality will tab past those new questions to whatever question happened to be next before those new questions were inserted.  Users should expect that if new questions appear, that the browser will tab directly to them and not skip over them.
***LEMsetTabIndexes()
****sets tabindex for all potentially visible form elements
****binds a keydown listener to each of them for managing TAB and SHIFT-TAB
*****calls ExprMgr_process_relevance_and_tailoring() to update question visibility and tailoring)
*****calls LEMmoveNextTabIndex() to moves to next relevant form element.
*****cancels the default processing of TAB or SHIFT-TAB
***LEMmoveNextTabIndex()
****For TAB, uses complex JQuery to get the tabindexes for the set of relevant and active form elements following the current element:
*****Find all questions that have tabindexes greater than the current tabindex (will include current question if there are other visible elements available within the question)
*****Fiters that set to only include relevant questions (the question's displaySGQA node has value="on")
*****Finds all relevant tabindexes within that set
*****Adds enabled button and submit buttons to that set
****Iterates through that set to find the first relevant tabindex after the current tabindex
****Cycles through the navigation buttons, and loops back to the top of the page if needed (skipping the browser search field)
****For SHIFT-TAB, does the equivalent, but to support movement backwards.
**JavaScript implementations of exposed EM functions, built to exactly mirror the server-side (PHP) functionality
***LEM*() - functions built by LimeSurvey team - these are typically functions that are not natively supported within PHP
***Javascript equivalent of existing PHP functions (so uses php names.  These are from [http://phpjs.org phpjs.org].

Latest revision as of 10:51, 15 April 2020

  Starting 2020, we renamed the Expression Manager to ExpressionScript.


This wiki page is meant for the LimeSurvey development team and others wishing to contribute to LimeSurvey. It provides details about how to work with, test, and extend ExpressionScript (ES).


  Please note that his document is currently incomplete.



Getting Started

The best way to get started with EM is to:

  • Install the latest version of LimeSurvey
  • Load and play with the ExpressionScript demos (located in /docs/demosurveys of the distribution)
  • Navigate through all of the test cases
    • To see the ExpressionScript displayed in the configuration toolbar, you must activate first the debug mode from application/config/config.php.
  • Read the documentation


EM Source Code Organization and Purpose

ExpressionScript Engine (EM)

Implements a recursive descent parser in PHP and JavaScript which let you create variables and securely expose a specified set of pre-defined functions.


Location

  • Version 1.92: /classes/expressions/ExpressionScript Engine.php
  • Versions 2.0 and 3.0: /application/helpers/expressions/em_core_helper.php


Recursive Descent Parser (all variables and functions starting with RDP_)

  • This is the core of the EM, a compiler which builds and evaluates the parse tree for the expression
  • Unless you are an expert in compiler theory, you should not touch the RDP_ code.


API Functions to the RDP

  • RegisterFunctions - an interface to add external functions to EM
  • sProcessStringContainingExpressions() - splits string on expressions, evaluates each, then joins together the parts
  • ProcessBooleanExpression() - evaluates whether an equation (not surrounded by curly braces) is true
  • Get*() - returns information about the most recently processed expressions


LimeExpressionScript Engine (LEM)

This centralizes the integration of LimeSurvey with EM.


Location

  • Version 1.92: /classes/expressions/LimeExpressionScript Engine.php
  • Versions 2.0 and 3.0: /application/helpers/expressions/em_manager_helper.php


Navigation Functions

These process the current responses, update the database, find the next relevant set of questions, and create the metadata needed to render those questions:

  • NavigateForwards()
  • NavigateBackwards()
  • JumpTo()
  • GetLastMoveResult()


Initialization Functions

  • StartSurvey() - initializes the survey, setting all variable mappings
  • setVariableAndTokenMappingsForExpressionScript Engine() - post-processes CreateFieldMap() to create metadata needed for all variables and token values
  • _CreateSubQLevelRelevanceAndValidationEquations() - processes advanced question attributes such as array_filter, min_answers, and min_value
  • ProcessAllNeededRelevance() - computes the relevance results (and JavaScript) for all applicable questions
  • _ProcessGroupRelevance() - computes the relevance of a group based upon its own relevance criteria and the status of the questions it contains.


Functions to Generate Tailored Content

  • StartProcessingPage() - called at beginning of each page
  • StartProcessingGroup() - called for each group on a page
  • FinishProcessingGroup() - collects all tailoring for this group so can render appropriate JavaScript
  • FinishProcessingPage() - completes the EM-related processing, and serializes LEM into $_SESSION['LEMsingleton']
  • GetRelevanceAndTailoringJavaScript() - collects all relevance, validation, and tailoring logic and generates ExprMgr_process_relevance_and_tailoring() JavaScript function


Response Management

  • ProcessCurrentResponses() - validates and processes the $_POSTed responses
  • _UpdateValuesInDatabase() - saves values to database, including NULLing irrelevant values


Caching

LEM tries to avoid calling the Initialization functions each page, storing state in $_SESSION['LEMsingleton']

  • SetDirtyFlag() - when called, tells LEM to force a rebuild of the Initialization functions (e.g. of admin has added/removed/updated any questions, groups, conditions, or defaults)
  • SetSurveyId()- calls SetDirtyFlag if the user wants to work with a different survey from the one that is cached
  • SetEMLanguage() - calls SetDirtyFlag() if the user starts to use a language different from the cached one


Validation Functions

  • _ValidateSurvey() - validates the entire survey by calling _ValidateGroup()
  • _ValidatGroup() - validates a group and returns its status by calling _ValidateQuestion() on all of its questions
  • _ValidateQuestion() - validates a question, determining whether it is relevant, hidden, or fails any validation and/or mandatory criteria


EM-related JavaScript

  • Location
    • Version 1.92: /classes/expressions/em_javascript.js
    • Versions 2.0 and 3.0: /scripts/admin/expressions/em_javascript.js
  • Purpose - contains all of the custom javascript needed for EM


JavaScript equivalents of PHP functions

About 20 functions from phpjs.org


Core Functions

  • LEManyNA() - checks whether any of the variables are irrelevant
  • LEMval() - retrieves the value for any variable, or its metadata (via the dot notation syntax)
  • LEMsetTabIndexes() - ensures that the tab sequence will act as expected even if input elements change visibility.


EM-related Test Cases

  • Location
    • Version 1.92: /classes/expressions/test/*.php
    • Versions 2.0 and 3.0: /application/views/admin/expressions/*
  • Purpose
    • Available Functions - runs EM::ShowAllowableFunctions to display functions and syntax from EM->RDP_ValidFunctions
    • String Splitter - runs EM::UnitTestStringSplitter() to show how it parses strings with curly braces
    • Tokenizer - runs EM::UnitTestTokenizer() to show how EM detects and categorizes tokens (e.g. variables, string, functions, operators)
    • Unit Tests of Isolated Expressions - runs EM::UnitTestEvaluator() for unit tests of each of ExpressionScript's features (e.g. all operators and functions). Color coding shows whether any tests fail. Syntax highlighting shows cases where ExpressionScript properly detects bad syntax.
    • Unit Tests of Expressions Within Strings - runs LEM::UnitTestProcessStringContainingExpressions() to show how ExpressionScript can process strings containing one or more variable, token, or expression replacements surrounded by curly braces.
    • Unit Test Dynamic Relevance Processing - runs LEM::UnitTestRelevance() to show how questions and substitutions should dynamically change based upon values entered.
    • Preview Conversion of Conditions to Relevance - runs LEM::UnitTestConvertConditionsToRelevance() to show relevance equations for all conditions in the database, grouped by question id
    • Bulk Convert Conditions to Relevance - actually performs the conversion, saving the generated relevance equations (while retaining the original conditions)
    • Test Navigation - unit tests LEM::NavigateForwards() for the selected survey, generating the following information based upon the selected debugging options:
      • Detailed Timing - shows low-level timing information for each part of EM (e.g. to examine duration of database calls)
      • Validation Summary - shows one line per group and question, showing its relevance equation, and indicating whether it was irrelevant, hidden, mandatory and/or failed validation criteria. Also shows the generated SQL per navigation step to update the database
      • Validation Detail - shows extra details per question, including
        • Validation Tip, Equation, JavaScript equivalent of the Equation
        • Lists of subquestions; which are relevant; and which are unanswered
        • List of array filters applied, by subquestion
      • Pretty Print Syntax - syntax highlights all of the equations so that you can see errors, and also click on variable names to jump to those questions and edit them.
    • Show Survey Logic File - generates the logic file which is available via the "QA" buttons in the admin console.


How EM Works

What is an Expression?

Anything surrounded by curly braces is an Expression, with two exceptions:

  • if there is whitespace after the opening brace or before the closing brace, it is ignored.
    • The EM can ignore in this way the embedded JavaScript.
    • So, if you have JavaScript that might be parsed by EM, make sure to add a space or newline after the opening brace.
  • Escaped curly braces are ignored (e.g. \{ and \})

Note that EM does support Expressions within strings. Moreover, Expressions can contain nested strings, but not nested expressions. So, the following red sections are valid Expressions and will cause substitions to occur within the containing strings.

  • <img src="images/mine_{Q1}.png">
  • <img src="images/mine_{if(Q1=="Y",'yes','no')}.png">
  • <img src="images/mine_{if(Q1=="Y",'single quote with {nested braces}',"double quote with {nested braces}")}.png">


What does EM do with text containing expressions?

  1. A regular expression divides the source line into STRING and EXPRESSION tokens
  2. Each EXPRESSION is parsed by ExpressionScript Engine, a recursive descent parser.
    1. If there are syntax errors, EM returns an HTML string that syntax-highlights the equation and puts red-lined boxes around syntax errors
    2. If there are no syntax errors, EM returns the result of evaluating the expression
  3. EM re-joins the STRING and EM-evaluated EXPRESSION parts.
  4. EM optionally appends the translation activity to structures used by GetRelevanceAndTailoringJavaScript().


How can we be sure that EM accurately parses the equations?

EM was originally written in 1999-2000 by Dr. Tom White (TMSWhite) for a different project (Dialogix) in Java, using JavaCC, an open source compiler compiler (parser generator). That Java-based project has been in production for over a decade, and has been fully vetted for unit and integration tests.

Since there is no production-grade parser generator for PHP and JavaScript (although Antlr is coming close), TMSWhite created a custom recursive descent parser for LimeSurvey. To ensure its accuracy, EM's logic is based upon the JavaCC source code for Dialogix. The JavaCC syntax mirrors the functionality needed for a recursive descent parser. JavaCC happens to build a state-based compiler, which is a little more efficient than a recusive descent parser. However, state-based compilers are impossible to read, understand, or expand without JavaCC-like source code, so it did not make sense to try to port the JavaCC output directly to PHP.

Futhermore, there are comprehensive unit and integration test suites for EM. These make it easy to validate the accuracy of the EM system. Each test suite includes dozens to hundreds of test cases, and it is trivial to add addition test cases.


How does the Recursive Descent Parser work?

EM must do the following:

  1. Tokenize the expression - separating strings, words (variable names vs. functions), and punctuation; and categorizing the types of each.
  2. Analyze the tokens to build a parse tree, checking for syntax errors along the way.
  3. Return the result of evaluating the expression (using PHP) (or return syntax-highlighted HTML if there are syntax errors).
  4. Create a safe JavaScript equivalent of the expression so that expressions can be dynamically re-computed client-side.
  5. Determine which variables are used in each expression (so can make sure they are available client-side).


How does EM integrate into LimeSurvey?

The LimeExpressionScript Engine (LEM) class manages the integration of EM into LimeSurvey. LEM must:

  1. Initialize all of the variables needed by LimeSurvey (e.g., for TOKENS, INSERTANS, and templatereplace())
  2. Know which Group and Question are being processed
  3. Record the results and metadata about all of the text that LimeSurvey asks it to process
  4. Output static HTML that reflects the results of that processing
  5. Output JavaScript that lets those results be dynamically re-computed if values on the page change.


Extending EM

Adding Functions

When you add a function that does not exist in PHP, add it also to the body of em_core_helper.php

FIXME: Eventually we should separate out such add-on functions into their own php file.

Functions are stored in LimeExpressionScript Engine::amValidFunctions[]. Some existing examples are:

'abs' => array('abs', 'Math.abs', 'Absolute value', 'number abs(number)', 'http://www.php.net/manual/en/function.checkdate.php', 1),
'if' => array('exprmgr_if', 'LEMif', 'Excel-style if(test,result_if_true,result_if_false)', 'if(test,result_if_true,result_if_false)', '', 3),
'max' => array('max', 'Math.max', 'Find highest value', 'number max(arg1, arg2, ... argN)', 'http://www.php.net/manual/en/function.max.php', -2),
'substr' => array('substr', 'substr', 'Return part of a string', 'string substr(string, start <nowiki>[</nowiki>, length])', 'http://www.php.net/manual/en/function.substr.php', 2,3)

The syntax for each function is:

function => array( detail_1, detail_2, detail_3, detail_4, detail_5, detail_6 )

The details which must be included in the array are:

  1. PHP function name - this is the PHP function that will be called for that func.
  2. JavaScript function name - this is the JavaScript function that will be called for that function
  3. Meaning - this is a short description of what the function does
  4. Syntax - this shows the valid syntax for the function
  5. Reference - this is an optional URL showing more details about the syntax (e.g. a link to the PHP documentation)
  6. Number of required arguments
    • Multiple values are allowed at the end of the array. For example, substr() above can take 2 or 3 arguments
    • Negative values mean that the function accepts a variable number of arguments
    • Negative values less than -1 mean that the function requires at least abs(N)-1 arguments (so -2 means it requires at least 1 argument)

If you add a function that does not exist in JavaScript, then add it to the body of em_javascript.js


Adding Test Cases

Please do! The testing frameworks are solid. You just need to add more tests following the examples in the code.

Make sure to add Unit tests for new functions to UnitTestEvaluator

  • Syntax is ExpectedResult~Expression, such as:
  • 212~5 + max(1,(2+3),(4 + (5 + 6)),[[]],[[7 + 8) + 9),( (10 + 11), 12),(13 + (14 * 15) - 16) )

When your test case should return an error, use NULL as the expected value, like this:

  • NULL~four * / seven
  • NULL~(5 + 7) = 8