From 25c99cc4c0da54b67df2abefba24a96e82dc6003 Mon Sep 17 00:00:00 2001 From: Maarten Duijndam Date: Fri, 13 Dec 2024 11:49:36 +0100 Subject: [PATCH 01/17] Add a grammar for stimuli --- build.bash | 3 + package.json | 17 ++++ plugins/grammar.ne | 149 +++++++++++++++++++++++++++++++ plugins/parts.js | 212 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 381 insertions(+) create mode 100755 build.bash create mode 100644 package.json create mode 100644 plugins/grammar.ne create mode 100644 plugins/parts.js diff --git a/build.bash b/build.bash new file mode 100755 index 0000000..66a93bd --- /dev/null +++ b/build.bash @@ -0,0 +1,3 @@ +#!/bin/bash + +npx nearleyc -o ./plugins/grammar.js ./plugins/grammar.ne diff --git a/package.json b/package.json new file mode 100644 index 0000000..23de4e6 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "jspsych-spr-mw", + "version": "1.0.0", + "description": "## A self paced reading with moving window experiment using jsPsych", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "./build.bash" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "moo": "^0.5.2", + "nearley": "^2.20.1" + } +} diff --git a/plugins/grammar.ne b/plugins/grammar.ne new file mode 100644 index 0000000..04c9cca --- /dev/null +++ b/plugins/grammar.ne @@ -0,0 +1,149 @@ + +@{% + import * as parts from "./parts"; + + import * as moo from 'moo'; + + const lexer = moo.compile({ + rec_group_start : "{{#" , + group_start : "{{" , + group_end : "}}" , + bold_start : /<[ ]*b[ ]*>/u , + bold_end : /<[ ]*[/]b[ ]*>/u , + italic_start : /<[ ]*i[ ]*>/u , + italic_end : /<[ ]*[/]i[ ]*>/u , + word : /\p{L}+/u , + ws : /[ \r\t]+/u , + newline : {match : "\n", lineBreaks: true}, + }) +%} + +@lexer lexer + +# We parse a number of groups to present to the user. +group_list -> + group # a single group can be a group_list + | group_list group # a group may be a group followed by more groups + | group_list WS group # allow white space between groups + | group_list WS # allow trailing white space + +# A group starts with {{ or {{# and ends with a}} we don't allow empty groups +# {{}} or {{#}} +group -> + %group_start sentence %group_end + | %rec_group_start sentence %group_end + + +sentence -> + Word {% + function(data) { + let sentence = new parts.SentenceList( + data[0].text_position, + ); + sentence.push(data[0]); + return sentence; + } + %} + | WS {% + function(data) { + let sentence = new parts.SentenceList( + data[0].text_position, + ); + sentence.push(data[0]); + return sentence; + } + %} + | special_sentence {% id %} + + | sentence Word {% + function(data) { + let sentence: parts.SentenceList; + sentence = data[0]; + sentence.push(data[1]); + return sentence; + } + %} + | sentence WS {% + function(data) { + let sentence: parts.SentenceList; + sentence = data[0]; + sentence.push(data[1]); + return sentence; + } + %} + | sentence special_sentence {% + function(data) { + let sentence: parts.SentenceList; + let special_sentence: parts.SentenceList; + + sentence = data[0]; + special_sentence = data[1]; + + function append_to_sentence( + part: parts.SentencePart) + { + sentence.parts.push(part); + } + + special_sentence.parts.forEach( + append_to_sentence + ) + return sentence; + } + %} + +special_sentence -> # sentences in bold/italics + bold_sentence {% id %} # id just returns the first element. + | italic_sentence {% id %} + +bold_sentence -> %bold_start sentence %bold_end {% + // drop italic_start and italic_end, return + // the sentence with all word marked italic. + function(data) { + let sentence_fragment: parts.SentenceList + sentence_fragment = data[1]; + sentence_fragment.mark_bold(); + return data[1]; // strip start and end + } + %} + +italic_sentence -> %italic_start sentence %italic_end {% + // drop italic_start and italic_end, return + // the sentence with all word marked italic. + function(data) { + let sentence_fragment: parts.SentenceList + sentence_fragment = data[1]; + sentence_fragment.mark_italian(); + return data[1]; // strip start and end + } + %} + +Word -> %word {% + function(data, pos) { + let word = new parts.Word( + data[0], + data[0].toString() + ); + return word; + } + %} + +WS -> + %ws {% + function(data, pos) { + let ws = new parts.WhiteSpace( + data[0], + data[0].toString() + ); + return ws; + } + %} + | %newline {% + function(data, pos) { + let ws = new parts.WhiteSpace( + data[0], + data[0].toString() + ); + return ws; + } + %} diff --git a/plugins/parts.js b/plugins/parts.js new file mode 100644 index 0000000..058a13d --- /dev/null +++ b/plugins/parts.js @@ -0,0 +1,212 @@ + +/** + * Class representing the position of a portion of the grammar + */ +export class PositionInfo { + + /** + * + * @param {object} tokenised Where the token is found by the lexer + * @param {number} tokenised.col + * @param {number} tokenised.line + * @param {number} tokenised.lineBreaks + * @param {number} tokenised.offset + */ + constructor(tokenised) { + try { + if (typeof tokenised.col !== "number") { + throw new TypeError("tokenised.column should be a number"); + } + if (typeof tokenised.line !== "number") { + throw new TypeError("tokenised.line should be a number"); + } + if (typeof tokenised.lineBreaks !== "number") { + throw new TypeError("tokenised.lineBreaks should be a number"); + } + if (typeof tokenised.offset !== "number") { + throw new TypeError("tokenised.offset should be a number"); + } + } catch (e) { + let breakpoint = e; + } + + this.col = tokenised.col; + this.line = tokenised.line; + this.lineBreaks = tokenised.lineBreaks; + this.offset = tokenised.offset; + } +} + +/** + * A grammar part is one symbol inside of the parsed text. + * It can be a terminal or non terminal part of the grammar. + */ +export class GrammarPart { + + /** + * + * @param {string} type_name What constinuent of the grammar this object is + * @param {PositionInfo} text_position Where is the constinuent in the parsed string + */ + constructor(type_name, text_position) { + if (typeof typename !== "string") { + throw new TypeError("type_name should be a string"); + } + if (typeof text_position !== "object") { + throw new TypeError("type_name should be an object"); + } + this.grammar_type_name = type_name; + this.text_position = new PositionInfo(text_position); + } + + /** + * + * @returns {string} What kind of symbol of the grammar this is + */ + get_type () { + return this.grammar_type_name; + } + + /** + * + * @returns {PositionInfo} Information about where this grammar symol + * is inside the text. + */ + get_position() { + return this.text_position; + } +} + +export class SentenceList extends GrammarPart { + + /** + * + * @param {PositionInfo} position + */ + constructor(position) { + super("SentenceList", position) + this.parts = []; + } + + /** + * + * @param {SentencePart} part + */ + push(part) { + if (part instanceof SentencePart) + this.parts.push(part); + else + throw new TypeError("Part was expected to be a SentencePart") + } + + mark_bold() { + this.parts.forEach( + function(value) { + if (value.grammar_type_name == "Word") { + wordval.mark_bold(); + } + } + ); + } + + mark_italian() { + this.parts.forEach( + function(value) { + if (value.grammar_type_name == "Word") { + value.mark_italian(); + } + } + ); + } +} + +export class SentencePart extends GrammarPart { + + /** + * + * @param {string} type + * @param {PositionInfo} position + * @param {string} content + */ + constructor(type, position, content) { + super(type, position); + if (typeof(content) !== "string") + throw TypeError("text is not a string."); + this.content = content; + } + + get_content() { + return this.content; + } +} + +export class Word extends SentencePart { + + bold = false; + italian = false; + + /** + * + * @param {PositionInfo} position + * @param {string} text + */ + + constructor (position, text) { + super("Word", position, text); + this.bold = false; + this.italic = false + } + + mark_bold() {this.bold = true;} + mark_italic() {this.italic = true;} +} + +export class WhiteSpace extends SentencePart { + + /** + * + * @param {PositionInfo} position + * @param {string} text + */ + constructor (position, text) { + super("WhiteSpace", position, text); + } +}; + + +export class Group extends GrammarPart { + + /** + * @param {PositionInfo} position + * @param {boolean} record + * @param {SentenceList} sentence_parts + */ + constructor(position, record, sentence_parts) { + super("Group", position); + if (typeof record !== "boolean") + throw new TypeError("Record was expected to be boolean") + this.record = record + this.sentence_parts = sentence_parts; + } +} + +export class GroupList extends GrammarPart { + /** + * + * @param {PositionInfo} position + */ + constructor (position) { + super("GroupList", position); + this.groups = [] + } + + /** + * Push a single group to the list of groups + * + * @param {Group} group + */ + push(group) { + this.groups.push(group) + } +}; + From d086e9c7f03d3ba81d58ded970e29cc2a961ed48 Mon Sep 17 00:00:00 2001 From: Maarten Duijndam Date: Mon, 16 Dec 2024 11:03:39 +0100 Subject: [PATCH 02/17] Setup npm project with nearley --- .gitignore | 6 ++++++ build.bash | 2 +- package.json | 3 ++- plugins/.gitignore | 3 +++ plugins/grammar.ne | 27 ++++++++++----------------- plugins/jspsych-spr-moving-window.js | 2 ++ 6 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 plugins/.gitignore diff --git a/.gitignore b/.gitignore index d103455..0ccc27a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,9 @@ upload-experiment.sh # Ignore the key used for testing purposes key.txt + +# node +node_modules + +#vs code +.vscode diff --git a/build.bash b/build.bash index 66a93bd..505713a 100755 --- a/build.bash +++ b/build.bash @@ -1,3 +1,3 @@ #!/bin/bash -npx nearleyc -o ./plugins/grammar.js ./plugins/grammar.ne +npx nearleyc -o ./plugins/grammar.js ./plugins/grammar.ne -e spr_grammar diff --git a/package.json b/package.json index 23de4e6..eb4b16b 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "version": "1.0.0", "description": "## A self paced reading with moving window experiment using jsPsych", "main": "main.js", + "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "./build.bash && node ./plugins/test.js", "build": "./build.bash" }, "keywords": [], diff --git a/plugins/.gitignore b/plugins/.gitignore new file mode 100644 index 0000000..512979c --- /dev/null +++ b/plugins/.gitignore @@ -0,0 +1,3 @@ + +# ignore grammar.js as it is generated from grammar.ne using nearley +grammar.js diff --git a/plugins/grammar.ne b/plugins/grammar.ne index 04c9cca..5d49732 100644 --- a/plugins/grammar.ne +++ b/plugins/grammar.ne @@ -1,8 +1,9 @@ @{% - import * as parts from "./parts"; - import * as moo from 'moo'; + const parts = require("./parts.js"); + const moo = require("moo"); +// import * as moo from "moo"; const lexer = moo.compile({ rec_group_start : "{{#" , @@ -57,30 +58,24 @@ sentence -> | sentence Word {% function(data) { - let sentence: parts.SentenceList; - sentence = data[0]; + let sentence = data[0]; sentence.push(data[1]); return sentence; } %} | sentence WS {% function(data) { - let sentence: parts.SentenceList; - sentence = data[0]; + let sentence = data[0]; sentence.push(data[1]); return sentence; } %} | sentence special_sentence {% function(data) { - let sentence: parts.SentenceList; - let special_sentence: parts.SentenceList; + let sentence = data[0]; + let special_sentence = data[1]; - sentence = data[0]; - special_sentence = data[1]; - - function append_to_sentence( - part: parts.SentencePart) + function append_to_sentence(part) { sentence.parts.push(part); } @@ -100,8 +95,7 @@ bold_sentence -> %bold_start sentence %bold_end {% // drop italic_start and italic_end, return // the sentence with all word marked italic. function(data) { - let sentence_fragment: parts.SentenceList - sentence_fragment = data[1]; + let sentence_fragment = data[1]; sentence_fragment.mark_bold(); return data[1]; // strip start and end } @@ -111,8 +105,7 @@ italic_sentence -> %italic_start sentence %italic_end {% // drop italic_start and italic_end, return // the sentence with all word marked italic. function(data) { - let sentence_fragment: parts.SentenceList - sentence_fragment = data[1]; + let sentence_fragment = data[1]; sentence_fragment.mark_italian(); return data[1]; // strip start and end } diff --git a/plugins/jspsych-spr-moving-window.js b/plugins/jspsych-spr-moving-window.js index 3ce0d5b..3e21be0 100644 --- a/plugins/jspsych-spr-moving-window.js +++ b/plugins/jspsych-spr-moving-window.js @@ -1,3 +1,5 @@ + + var sprMovingWindow = (function(jspsych) { const SPR_MW_PLUGIN_NAME = 'spr-moving-window'; From 8d3dac02930e55a092c90a5288722a3269bc4594 Mon Sep 17 00:00:00 2001 From: Maarten Duijndam Date: Mon, 16 Dec 2024 11:03:59 +0100 Subject: [PATCH 03/17] Wish list of features to add --- index.html | 2 ++ main.js | 1 + stimuli.js | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 73d37b5..8fb1be6 100644 --- a/index.html +++ b/index.html @@ -29,6 +29,8 @@ + +