diff --git a/demos/src/basic.mustache b/demos/src/basic.mustache index 51bb248..b5e8bf3 100644 --- a/demos/src/basic.mustache +++ b/demos/src/basic.mustache @@ -9,44 +9,50 @@ publisher: Financial Times pubdate: 2016/03/02 size: 15x15 across: -- [1,1] 1. Wise king therefore left daughter on lock (XXXXXX) -- [8,1] 4. Healing ointment spoken of at castle (XXXXXXXX) -- [1,3] 9. Remain around township near Wrexham (XXXXXX) -- [8,3] 10. Island off Senegal, unknown (XXXXXXXX) -- [1,5] 11. Posh car came in. Yes! Reversed into coach in country (XXXXXX) -- [8,5] 12. Pressure to take measure against bankrupt Welsh market town (XXXXXXXX) -- [7,6] 13. Call back in Shropshire town (XXX) -- [1,7] 14. Oil refiner backed race in Yorkshire market town (XXXXXX) -- [5,8] 17. Meat and food generally brought back to German port (XXXXXXX) -- [10,9] 21. Lands a part in amateur opera (XXXXXX) -- [7,10] 25. Tailless stag on celebrated salmon-fishing river (XXX) -- [1,11] 26. Two states having parties for department (XXXXXXXX) -- [10,11] 27. Little brother, initially uncertain, gets time off in Belgian city (XXXXXX) -- [1,13] 28. Tin reels sorted in province (XXXXXXXX) -- [10,13] 29. Eastender's murder in village on Loch 15 (XXXXXX) -- [1,15] 30. Grant's returned to employment at Sicillian port (XXXXXXXX) -- [10,15] 31. Note about a statue in African country (XXXXXX) +- (1,1) 1. Wise king therefore left daughter on lock (XXXXXX) +- (8,1) 4. Healing ointment spoken of at castle (XXXXXXXX) +- (1,3) 9. Remain around township near Wrexham (XXXXXX) +- (8,3) 10. Island off Senegal, unknown (XXXXXXXX) +- (1,5) 11. Posh car came in. Yes! Reversed into coach in country (XXXXXX) +- (8,5) 12. Pressure to take measure against bankrupt Welsh market town (XXXXXXXX) +- (7,6) 13. Call back in Shropshire town (XXX) +- (1,7) 14. Oil refiner backed race in Yorkshire market town (XXXXXX) +- (5,8) 17. Meat and food generally brought back to German port (XXXXXXX) +- (10,9) 21. Lands a part in amateur opera (XXXXXX) +- (7,10) 25. Tailless stag on celebrated salmon-fishing river (XXX) +- (1,11) 26. Two states having parties for department (XXXXXXXX) +- (10,11) 27. Little brother, initially uncertain, gets time off in Belgian city (XXXXXX) +- (1,13) 28. Tin reels sorted in province (XXXXXXXX) +- (10,13) 29. Eastender's murder in village on Loch 15 (XXXXXX) +- (1,15) 30. Grant's returned to employment at Sicillian port (XXXXXXXX) +- (10,15) 31. Note about a statue in African country (XXXXXX) down: -- [1,1] 1. Capital's got the sun in Cyprus (XXXXXXXX) -- [3,1] 2. Royal burgh and resort churlish about conservationists (XXXXXXXX) -- [5,1] 3. Runs into the barn, drunk, in 12's country (XXXXXXXX) -- [9,1] 5. Ran round edge of Dutch city (XXXXXX) -- [11,1] 6. Amateur politician blocking letter in West Coast resort (XXXXXX) -- [13,1] 7. Russian town having roots around Volga, originally (XXXXXX) -- [15,1] 8. Field study in Dutch city (XXXXXX) -- [8,5] 12. Bleepers haven't started ringing out in another royal burgh (XXXXXXX) -- [6,7] 15. Another noted salmon river in west Ayrshire (XXX) -- [10,7] 16. Quartet regularly at Wensleydale river (XXX) -- [11,8] 18. Oil painting of first and last halves of islands and Basque city (XXXXXXXX) -- [13,8] 19. Marks on top-class trademark raised around new country (XXXXXXXX) -- [15,8] 20. Chap left to abandon one capital (XXXXXXXX) -- [1,10] 22. Book sliced in half Greater Manchester (XXXXXX) -- [3,10] 23. Department one has to go round in France (XXXXXX) -- [5,10] 24. British current holders of Rome's art in wine-making region (XXXXXX) -- [7,10] 25. 7's river set around river in Ukraine (XXXXXX) +- (1,1) 1. Capital's got the sun in Cyprus (XXXXXXXX) +- (3,1) 2. Royal burgh and resort churlish about conservationists (XXXXXXXX) +- (5,1) 3. Runs into the barn, drunk, in 12's country (XXXXXXXX) +- (9,1) 5. Ran round edge of Dutch city (XXXXXX) +- (11,1) 6. Amateur politician blocking letter in West Coast resort (XXXXXX) +- (13,1) 7. Russian town having roots around Volga, originally (XXXXXX) +- (15,1) 8. Field study in Dutch city (XXXXXX) +- (8,5) 12. Bleepers haven't started ringing out in another royal burgh (XXXXXXX) +- (6,7) 15. Another noted salmon river in west Ayrshire (XXX) +- (10,7) 16. Quartet regularly at Wensleydale river (XXX) +- (11,8) 18. Oil painting of first and last halves of islands and Basque city (XXXXXXXX) +- (13,8) 19. Marks on top-class trademark raised around new country (XXXXXXXX) +- (15,8) 20. Chap left to abandon one capital (XXXXXXXX) +- (1,10) 22. Book sliced in half Greater Manchester (XXXXXX) +- (3,10) 23. Department one has to go round in France (XXXXXX) +- (5,10) 24. British current holders of Rome's art in wine-making region (XXXXXX) +- (7,10) 25. 7's river set around river in Ukraine (XXXXXX) # -# [coordinates of clue in grid]: [across,down]. [1,1] = top left, [17,17]=bottom right. -# (WORDS,IN,ANSWER): capitalised, and separated by commas or hyphens. +# Notes on the text format... +# Can't use square brackets or speech marks. +# A clue has the form +# - (COORDINATES) ID. Clue text (ANSWER) +# Coordinates of clue in grid are (across,down), so (1,1) = top left, (17,17) = bottom right. +# ID is a number, followed by a full stop. +# (WORDS,IN,ANSWER): capitalised, and separated by commas or hyphens, or (numbers) separated by commas or hyphens. +# ANSWERS with all words of XXXXXX are converted to numbers. ---">
diff --git a/src/js/crossword_parser.js b/src/js/crossword_parser.js index 4962672..9e2b547 100644 --- a/src/js/crossword_parser.js +++ b/src/js/crossword_parser.js @@ -1,156 +1,3 @@ - // Given the json text of a crossword spec, generate the equivalent DSL, - // bailing as soon as an error is found. - // Only enough error checking is done to ensure the DSL can be constructed, - // since it is assumed the DSL will itself be checked subsequently - // to see if it specifies a valid crossword. - function parseJsonIntoDSL(text) { - var dslLines = []; - var errors = []; - var response = { - errors : errors, - dslText : "", - }; - var json; - - function addError(e){ - errors.push( "ERROR: " + e ); - } - - function responseWithError(e) { - errors.unshift('Assuming this is a JSON doc...'); - if (e) { - addError( e ); - } - return response; - } - - try { - json = JSON.parse(text); - } - catch(err) { - return responseWithError( err.message ); - } - - // check the simple, single-value fields - ['author','editor','publisher','copyright','date'].forEach( f => { - if (f in json) { - let name = f; - if (f === 'date') { - name = 'pubdate'; - } - dslLines.push( name + ' ' + json[f] ); - } else { - addError( `missing field: ${f}` ); - } - }); - - // check for the complex fields - // except 'answers' (for now) - ['size','grid','gridnums','clues'].forEach( f => { - if (! (f in json) ) { - addError( `missing field: ${f}` ); - } - }); - - if (errors.length > 0) { - return responseWithError(); - } - - if (json.size.rows && json.size.cols) { - dslLines.push(`size ${json.size.rows}x${json.size.cols}`); - } else { - return responseWithError('could not parse size rows and cols'); - } - - if (json.gridnums.length !== json.size.rows){ - return responseWithError('gridnums does not match size.rows'); - } - - let idCoordinates = {}; - - for( let [r, row] of json.gridnums.entries() ) { - if (row.length !== json.size.cols){ - return responseWithError(`gridnums row ${r+1} does not match size.cols`); - } - for( let [c, cell] of row.entries() ) { - if(cell !== 0){ - if (cell in idCoordinates) { - return responseWithError(`duplicate id in gridnums: [${r+1},${c+1}] ${cell}`); - } - idCoordinates[cell] = [c,r]; - } - } - } - - let answers; - - if (json.answers) { - for( let grouping of ['across', 'down'] ) { - if(! json.answers[grouping]){ - return responseWithError(`could not find answers.${grouping}`); - } - } - - answers = json.answers; - } - - for( let grouping of ['across', 'down'] ) { - if(! json.clues[grouping]){ - return responseWithError(`could not find clues.${grouping}`); - } - if(answers && (json.clues[grouping].length !== answers[grouping].length)) { - return responseWithError(`mismatch between answers and clues in grouping ${grouping}`); - } - - dslLines.push(grouping); - - for( let [i, c] of json.clues[grouping].entries()) { - let id = c[0]; - if (! (id in idCoordinates)) { - return responseWithError(`no gridnums value for clue ${id} ${grouping}`); - } - - // there was a bug in the spec which seems to have resulted in some - // instances containing a mix of integers and strings here, - // so stripping out non integers - let wordSizes = c[2].filter(Number.isInteger); - - // if we only have the answer sizes, mock up a string consisting entirely of Xs - let answerCombined; - if (answers) { - answerCombined = answers[grouping][i]; - } else { - answerCombined = wordSizes.map(s => 'X'.repeat(s) ).join(''); - } - - // then split the text into the correctly-sized words. - let letters = answerCombined.split(''); - let words = wordSizes.map(s => letters.splice(0, s).join('')); - let wordsCSV = words.join(','); - - let body = c[1]; - - let clue = [ - `[${idCoordinates[id][0]+1},${idCoordinates[id][1]+1}]`, - `${id}.`, - body, - `(${wordsCSV})` - ].join(' '); - - dslLines.push(clue); - } - } - - if( errors.length > 0 ) { - addError("having attempted to catch all the errors, should not reach this point with any remaining errors"); - return responseWithError(); - } - - response['dslText'] = dslLines.join("\n"); - - return response; - } - // given the DSL, ensure we have all the relevant pieces, // and assume there will be subsequent checking to ensure they are valid function parseDSL(text){ @@ -178,10 +25,11 @@ // strip out trailing and leading spaces line = line.trim(); - if ( line === "" ) { /* ignore blank lines */ } - else if( line === "---") { /* ignore front matter lines */ } + if ( line === "" ) { /* ignore blank lines */ } + else if( line === "---") { /* ignore front matter lines */ } + else if (match = /^(layout|tag|tags|permalink):\s/ .exec(line) ) { /* ignore front matter fields */ } else if (match = /^version:?\s+(.+)$/i .exec(line) ) { crossword.version = match[1]; } - else if (match = /^name:?\s+(.+)$/i .exec(line) ) { crossword.name = match[1]; } + else if (match = /^name:?\s+(.+)$/i .exec(line) ) { crossword.name = match[1]; } else if (match = /^author:?\s+(.+)$/i .exec(line) ) { crossword.author = match[1]; } else if (match = /^editor:?\s+(.+)$/i .exec(line) ) { crossword.editor = match[1]; } else if (match = /^copyright:?\s+(.+)$/i .exec(line) ) { crossword.copyright = match[1]; } @@ -189,7 +37,7 @@ else if (match = /^pubdate:?\s+(\d{4}\/\d\d\/\d\d)$/i.exec(line) ) { crossword.pubdate = match[1]; } else if (match = /^(?:size|dimensions):?\s+(15x15|17x17)$/i.exec(line) ) { crossword.dimensions = match[1]; } else if (match = /^(across|down):?$/i .exec(line) ) { cluesGrouping = match[1]; } - else if (match = /^(?:\s*-\s)?\[(\d+),(\d+)\]\s+(\d+)\.\s+(.+)\s+\(([A-Z,-]+|[0-9,-]+)\)$/.exec(line) ) { + else if (match = /^-\s\((\d+),(\d+)\)\s+(\d+)\.\s+(.+)\s+\(([A-Z,-]+|[0-9,-]+)\)$/.exec(line) ) { if (! /(across|down)/.test(cluesGrouping)) { crossword.errors.push("ERROR: clue specified but no 'across' or 'down' grouping specified"); break; @@ -198,7 +46,7 @@ coordinates : [ parseInt(match[1]), parseInt(match[2]) ], id : parseInt(match[3]), body : match[4], - answerCSV : match[5], // could be either "A,LIST-OF,WORDS" or "1,4-2,5" + answerCSV : match[5], // could be in the form of either "A,LIST-OF,WORDS" or "1,4-2,5" original : line, }; crossword[cluesGrouping].push(clue); @@ -239,8 +87,14 @@ down : [] }; + // insist on having at least one clue ! + if ( (crossword['across'].length + crossword['down'].length) == 0) { + crossword.errors.push("Error: no valid clues specified"); + } + for(let grouping of ['across', 'down']){ let prev = groupingPrev[grouping]; + for(let clue of crossword[grouping]){ function clueError(msg){ crossword.errors.push("Error: " + msg + " in " + grouping + " clue=" + clue.original); @@ -328,16 +182,6 @@ }); } - // let answerPieces = clue.answerCSV.split(/([A-Z]+|[,-])/); - // let answerSpecPieces = answerPieces.map(function(p){ - // if (/[A-Z]+/.exec(p)) { - // return p.length; - // } else { - // return p; - // } - // }); - // clue.answerSpec = answerSpecPieces.join(''); - // check answer + offset within bounds if( (grouping==='across' && (clue.wordsString.length + x - 1 > maxCoord)) || (grouping==='down' && (clue.wordsString.length + y - 1 > maxCoord)) ){ @@ -544,7 +388,7 @@ crossword[grouping].forEach( clue => { var pieces = [ '-', - `[${clue.coordinates.join(',')}]`, + `(${clue.coordinates.join(',')})`, `${clue.id}.`, clue.body, `(${(withAnswers)? clue.answerCSV : clue.numericCSV})` @@ -555,8 +399,14 @@ var footerComments = [ '', - '[coordinates of clue in grid]: [across,down]. [1,1] = top left, [17,17]=bottom right.', - '(WORDS,IN,ANSWER): capitalised, and separated by commas or hyphens.' + "Notes on the text format...", + "Can't use square brackets or speech marks.", + "A clue has the form", + "- (COORDINATES) ID. Clue text (ANSWER)", + "Coordinates of clue in grid are (across,down), so (1,1) = top left, (17,17) = bottom right.", + "ID is a number, followed by a full stop.", + "(WORDS,IN,ANSWER): capitalised, and separated by commas or hyphens, or (numbers) separated by commas or hyphens.", + "ANSWERS with all words of XXXXXX are converted to numbers.", ]; lines = lines.concat( footerComments.map(c => { return `# ${c}`; } ) ); @@ -580,9 +430,9 @@ // only attempt to validate the crossword if no errors found so far if (crossword.errors.length == 0) { crossword = validateAndEmbellishCrossword(crossword); - console.log("validated crossword=", crossword); + console.log("parseWhateverItIs: validated crossword"); } else { - console.log("could not validate crossword=", crossword); + console.log("parseWhateverItIs: did not validate crossword=", crossword); } // generate the spec, and specTexts with and without answers @@ -605,11 +455,12 @@ crossword.gridText = generateGridText( crossword ); if (crossword.errors.length == 0) { + console.log('parseWhateverItIs: no errors so generated DSLs'); let withAnswers = true; crossword.DSLGeneratedFromDSLWithAnswers = generateDSL( crossword, withAnswers ); - console.log('crossword.DSLGeneratedFromDSLWithAnswers:', crossword.DSLGeneratedFromDSLWithAnswers); crossword.DSLGeneratedFromDSLWithoutAnswers = generateDSL( crossword, ! withAnswers ); - console.log('crossword.DSLGeneratedFromDSLWithoutAnswers:', crossword.DSLGeneratedFromDSLWithoutAnswers); + } else { + console.log( "parseWhateverItIs: errors found:\n", crossword.errors.join("\n") ); } return crossword; @@ -621,17 +472,18 @@ var responseObj; if (crossword.errors.length == 0) { + console.log("parseWhateverItIsIntoSpecText: no errors found"); responseObj = crossword.spec; } else { responseObj = { errors: crossword.errors, text : text } + console.log("parseWhateverItIsIntoSpecText: errors found:\n", crossword.errors.join("\n"), "\ntext=\n", text); } var jsonText = JSON.stringify( responseObj ); - console.log("parseWhateverItIsIntoSpecText: crossword=", crossword, ", jsonText=", jsonText ); return jsonText; }