diff --git a/package-lock.json b/package-lock.json index 20ff52745..22e95a5a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "cross-var": "^1.1.0", "csv-parser": "^3.0.0", + "csv-writer": "^1.6.0", "fs-extra": "^11.1.0", "json-beautify": "^1.1.1", "minimist": "^1.2.8", @@ -1501,6 +1502,11 @@ "node": ">= 10" } }, + "node_modules/csv-writer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/csv-writer/-/csv-writer-1.6.0.tgz", + "integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -6006,6 +6012,11 @@ "minimist": "^1.2.0" } }, + "csv-writer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/csv-writer/-/csv-writer-1.6.0.tgz", + "integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/package.json b/package.json index d99ee89d5..3f41e5e66 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,13 @@ "create-all-tests": "cross-var node scripts/create-all-tests.js --testplan=$npm_config_testplan", "review-tests": "cross-var node --experimental-modules scripts/test-reviewer.mjs --testplan=$npm_config_testplan", "update-reference": "node scripts/update-reference.js", + "make-v2": "node scripts/v2maker.js", "prepare": "husky install" }, "dependencies": { "cross-var": "^1.1.0", "csv-parser": "^3.0.0", + "csv-writer": "^1.6.0", "fs-extra": "^11.1.0", "json-beautify": "^1.1.1", "minimist": "^1.2.8", diff --git a/scripts/v2maker.js b/scripts/v2maker.js new file mode 100644 index 000000000..5797d6810 --- /dev/null +++ b/scripts/v2maker.js @@ -0,0 +1,636 @@ +const fs = require('fs'); +const path = require('path'); +const csv = require('csv-parser'); +const createCsvWriter = require('csv-writer').createObjectCsvWriter; +const util = require(path.join(__dirname, 'v2makerUtil.js')); +const v2json = require(path.join(__dirname, 'v2maker.json')); + +// inputs +const csvInputFiles = new Map([ + ['v1tests', 'testsV1.csv'], + ['v1commands', 'commands.csv'], + ['v1references', 'referencesV1.csv'], + ['assertionSubstitutions', path.join(__dirname, 'v2substitutionsForAssertionIds.csv')], + ['testSubstitutions', path.join(__dirname, 'v2substitutionsForTestIds.csv')], + ['commandSubstitutions', path.join(__dirname, 'v2substitutionsForCommands.csv')], +]); + +// outputs +let testsFile = 'tests.csv'; +let assertionsFile = 'assertions.csv'; +let jawsCommandsFile = 'jaws-commands.csv'; +let nvdaCommandsFile = 'nvda-commands.csv'; +let voCommandsFile = 'voiceover_macos-commands.csv'; +let scriptsFile = 'scripts.csv'; +let referencesFile = 'references.csv'; + +const args = require('minimist')(process.argv.slice(2), { + alias: { + h: 'help', + }, +}); + +if (args.help) { + console.log(` + v2maker requires one argument: the name of the test plan directory. + The data subdirectory of the test plan directory must contain: + test.csv + commands.csv + references.csv + + If backup copies of the original V1 format tests.csv and references.csv have not been created by a prior run, the tests.csv and references.csv will be copied to testsV1.csv and referencesV1.csv before their content is replaced with the new V2 formatted content. + + Arguments: + -h, --help + Show this message. + `); + process.exit(); +} + +if (args._.length !== 1) { + console.log( + `Name of a test plan directory that is a subdirectory of the 'tests' directory is required.` + ); + process.exit(); +} + +main(); + +async function main() { + try { + const testPlan = args._[0]; + testDataPath = path.join(__dirname, '..', 'tests', testPlan, 'data'); + await util.startLoggingTo(path.join(testDataPath, 'log.txt'), 'both'); + await setupFilePaths(testDataPath); + const csvData = await readCsvFiles(); + const haveRequiredData = await isV1dataAvailable(csvData); + if (!haveRequiredData) process.exit(1); + let assertionRows = await makeAssertionsCsvData(csvData); + const v1ToV2testIdMap = await makeTestsCsvData(csvData, assertionRows); + await makeCommandsCsvData(csvData, v1ToV2testIdMap); + await makeScriptsCsvData(csvData); + await makeReferencesCsvData(csvData); + await util.logMessage(`\nFinished conversion at ${new Date().toISOString()}`); + } catch (error) { + console.error('Error!', error); + } +} + +async function setupFilePaths(testDataPath) { + // Set absolute file paths for all inputs and outputs. + // Also, if the v1 tests and references files have not been copied to preserve their content, perform the copy operation. + + // Configure file paths for the files that are in the test plan data directory. + // inputs + csvInputFiles.set('v1tests', path.join(testDataPath, csvInputFiles.get('v1tests'))); + csvInputFiles.set('v1commands', path.join(testDataPath, csvInputFiles.get('v1commands'))); + csvInputFiles.set('v1references', path.join(testDataPath, csvInputFiles.get('v1references'))); + + // outputs + testsFile = path.join(testDataPath, testsFile); + assertionsFile = path.join(testDataPath, assertionsFile); + jawsCommandsFile = path.join(testDataPath, jawsCommandsFile); + nvdaCommandsFile = path.join(testDataPath, nvdaCommandsFile); + voCommandsFile = path.join(testDataPath, voCommandsFile); + scriptsFile = path.join(testDataPath, scriptsFile); + referencesFile = path.join(testDataPath, referencesFile); + + // If the tests and references files have not yet been copied to testsV1.csv and referencesV1.csv, copy them. + // The content of the original v1 files will be overwritten with the new v2 content when making the v2 conversion. + if (!fs.existsSync(csvInputFiles.get('v1tests'))) { + try { + await fs.promises.copyFile(testsFile, csvInputFiles.get('v1tests')); + const logMsg = `Copied ${path.basename(testsFile)} to ${path.basename( + csvInputFiles.get('v1tests') + )}`; + await util.logMessage(logMsg); + } catch (copyErr) { + throw new Error(`Error copying ${testsFile} to ${csvInputFiles.get('v1tests')}: ${copyErr}`); + } + } + if (!fs.existsSync(csvInputFiles.get('v1references'))) { + try { + await fs.promises.copyFile(referencesFile, csvInputFiles.get('v1references')); + const logMsg = `Copied ${path.basename(referencesFile)} to ${path.basename( + csvInputFiles.get('v1references') + )}`; + await util.logMessage(logMsg); + } catch (copyErr) { + throw new Error( + `Error copying ${referencesFile} to ${csvInputFiles('v1references')}: ${copyErr}` + ); + } + } +} + +async function readCsvFiles() { + const csvData = {}; + const filePromises = Array.from(csvInputFiles.entries()).map( + async ([fileDescriptor, fileName]) => { + const stream = fs.createReadStream(fileName, 'utf8'); + const rows = []; + return new Promise((resolve, reject) => { + stream + .pipe(csv()) + .on('data', row => { + rows.push(row); + }) + .on('end', () => { + csvData[fileDescriptor] = rows; + resolve(); + }) + .on('error', error => { + reject(error); + }); + }); + } + ); + + await Promise.all(filePromises); + return csvData; +} + +async function isV1dataAvailable(allInputCsvData) { + /** + * Make sure the testsV1.csv has V1 columns. + * If it does not, assume it contains V2 data and the V1 data is not available in the data directory. + * If V1 data is not available, delete the testsV1.csv file that was most likely created by the setupFilePaths method. + */ + + let v1dataAvailable = true; + + // Make sure that the testsV1 columns include the following. Require only 1 assertion column. + const v1columns = [ + 'testId', + 'title', + 'appliesTo', + 'mode', + 'task', + 'setupScript', + 'setupScriptDescription', + 'refs', + 'instructions', + 'assertion1', + ]; + let missingColumns = []; + const inputRow = allInputCsvData.v1tests[0]; + for (column of v1columns) { + if (!(column in inputRow)) missingColumns.push(column); + } + if (missingColumns.length > 0) { + v1dataAvailable = false; + await util.deleteFile(csvInputFiles.get('v1tests')); + await util.logMessage(` + ${path.basename(csvInputFiles.get('v1tests'))} is missing the following columns: ${missingColumns} + Deleted ${path.basename(csvInputFiles.get('v1tests'))}. + `); + } + + return v1dataAvailable; +} + +async function makeAssertionsCsvData(allInputCsvData) { + await util.logMessage(`\nCreating ${path.basename(assertionsFile)}`); + + // pull data from the assertion1 through assertionN columns of the v1tests array in allInputCsvData. + let assertions = []; + for (const row of allInputCsvData.v1tests) { + // properties in the row objects match columns in the input file: + // testId,title,appliesTo,mode,task,setupScript,setupScriptDescription,refs,instructions,assertion1,assertion2,assertion3,assertion4,assertion5,assertion6,assertion7 + const colNames = Object.keys(row); + for (const colName of colNames) { + if (colName.startsWith('assertion')) { + if (row[colName].trim() !== '') { + assertions.push(row[colName]); + } + } + } + } + + // Separate p1 and p2 assertions. + let p1assertions = assertions.filter(assertion => !assertion.includes('2:')); + let p2assertions = assertions.filter(assertion => assertion.includes('2:')); + // Remove the prefixes from the p2 assertions. + p2assertions = p2assertions.map(assertion => assertion.replace('2:', '')); + // Get rid of duplicates. + p1assertions = [...new Set(p1assertions)]; + p2assertions = [...new Set(p2assertions)]; + // Remove p1 assertions that are the same as a p2 assertion. + for (const p2assertion of p2assertions) { + if (p1assertions.includes(p2assertion)) { + await util.logMessage( + `P1 and P2 priorities found for assertion '${p2assertion}. Keeping only the P2 priority.` + ); + p1assertions = p1assertions.filter(p1assertion => p1assertion !== p2assertion); + } + } + + // to make assertionIds, we need the substitutions and deletions data. + const { deletions, substitutions } = util.getSubstitutionsAndDeletions( + 'assertionSubstitutions', + allInputCsvData, + 'oldWords', + 'newWords' + ); + + // Start making rows for the output CSV file. + let assertionRows = []; + for (const assertion of p1assertions) { + let newRow = {}; + newRow.assertionId = util.makeId(util.trimQuotes(assertion), substitutions, deletions); + newRow.priority = '1'; + newRow.assertionStatement = util.sentenceCase(assertion); + const phrase = assertion.replace( + /("?)(\w)((?:.*?),?(?:.*?))((?:,\s+is|\s+is)\s+conveyed)("?)/g, + '$1Convey $2$3$5' + ); + newRow.assertionPhrase = util.phraseCase(phrase); + newRow.refIds = ''; + assertionRows.push(newRow); + } + for (const assertion of p2assertions) { + let newRow = {}; + newRow.assertionId = util.makeId(util.trimQuotes(assertion), substitutions, deletions); + newRow.priority = '2'; + newRow.assertionStatement = util.sentenceCase(assertion); + const phrase = assertion.replace( + /("?)(\w)((?:.*?),?(?:.*?))((?:,\s+is|\s+is)\s+conveyed)("?)/g, + '$1Convey $2$3$5' + ); + newRow.assertionPhrase = util.phraseCase(phrase); + newRow.refIds = ''; + assertionRows.push(newRow); + } + + // Sort rows by assertionId ascending + assertionRows.sort((a, b) => { + const valueA = a.assertionId; + const valueB = b.assertionId; + if (valueA < valueB) { + return -1; + } + if (valueA > valueB) { + return 1; + } + return 0; + }); + + // Write rows to CSV file + const rowWriter = createCsvWriter({ + path: assertionsFile, + header: Object.keys(assertionRows[0]).map(column => ({ id: column, title: column })), + }); + await rowWriter.writeRecords(assertionRows); + await util.logMessage( + `Wrote ${assertionRows.length} assertion statements to ${path.basename(assertionsFile)}` + ); + + return assertionRows; +} + +async function makeTestsCsvData(allInputCsvData, assertionRows) { + await util.logMessage(`\nCreating ${path.basename(testsFile)}.`); + + // to make testIds, we will need the substitutions and deletions data. + const { deletions, substitutions } = util.getSubstitutionsAndDeletions( + 'testSubstitutions', + allInputCsvData, + 'oldWords', + 'newWords' + ); + + // To convert assertions to assertionIds, we will need a map of assertionStatements to assertionIds. So comparisons can be case insensitive, we'll lowercase the keys. + const assertionStatementsToIds = new Map(); + for (row of assertionRows) { + assertionStatementsToIds.set(row.assertionStatement.toLowerCase(), row.assertionId); + } + + // pull the data for the rows of the new tests file from the columns of the v1tests array in allInputCsvData. + let v2testRows = []; + for (const row of allInputCsvData.v1tests) { + // properties in the row objects match columns in the input file: + // testId,title,appliesTo,mode,task,setupScript,setupScriptDescription,refs,instructions,assertion1,assertion2,assertion3,assertion4,assertion5,assertion6,assertion7 + let newRow = {}; + // Add properties to newRow in the order that the columns will appear in the new tests.csv file. + newRow.testId = util.makeId(util.trimQuotes(row.title), substitutions, deletions); + newRow.title = util.sentenceCase( + row.title.replace(/read\s+information\s+about/i, 'Request information about') + ); + newRow.presentationNumber = row.testId; + newRow.setupScript = row.setupScript; + newRow.instructions = row.instructions; + let assertions = ''; + // Loop through the assertion columns, get the assertionId for each, and join them with spaces. + const colNames = Object.keys(row); + for (const colName of colNames) { + if (colName.startsWith('assertion')) { + if (row[colName].trim() !== '') { + // Remove priority prefix if present. + const v1assertion = row[colName].replace('2:', ''); + // get the v2 assertion Id and add it to the assertions string. Do lookups and comparisons using lowercase. Keeping the original case for the log in the event the assertion is not found. + if (assertionStatementsToIds.has(v1assertion.toLowerCase())) { + assertions += ` ${assertionStatementsToIds.get(v1assertion.toLowerCase())}`; + } else + await util.logMessage( + `assertionId not found for '${v1assertion} when processing testId ${row.testId}.` + ); + } + } + } + newRow.assertions = assertions.trim(); + v2testRows.push(newRow); + } + + // Sort rows by presentationNumber ascending + v2testRows.sort((a, b) => { + const valueA = Number(a.presentationNumber); + const valueB = Number(b.presentationNumber); + if (valueA < valueB) { + return -1; + } + if (valueA > valueB) { + return 1; + } + return 0; + }); + + /* + We will be filtering testRows to keep only the rows for the VoiceOver tests. We want to get rid of all the tests that specify reading mode or interaction mode. + However, before removing those obsolete tests, there is some data we need to collect from them: + 1. A map of all the v1 testIds to v2 testIds to use during conversion of commands.csv. + 2. Determine whether there are test tasks where JAWS and NVDA have assertions that are not specified for VoiceOver. + If there are tests where JAWS and NVDA have assertions that are not specified for VoiceOver, we'll log a warning for each task/assertion pair specified for JAWS and NVDA that is not specified for VoiceOver. + + Note: It's better to log a warning than combine all the assertions that appear to be different because they might not actually be different in meaning. + Sometimes there are two different wordings of the same assertion. + */ + // Build a map that specifies which new v2testId to use in place of a v1testId in commands.csv. + const v1ToV2testIds = new Map(); + for (row of v2testRows) { + v1ToV2testIds.set(row.presentationNumber, row.testId); + } + + // Get a set containing all the tasks in the test plan. + const v1tasks = new Set(allInputCsvData.v1tests.map(row => row.task)); + // iterate through the tasks and compare the VoiceOver assertions to the the JAWS and NVDA assertions. + for (const task of v1tasks) { + // Find the v1testId for the VoiceOver test for this task in the v1testRows array. + const voiceOverV1TestId = allInputCsvData.v1tests.find( + row => row.task === task && row.appliesTo.toLowerCase().includes('voiceover') + ).testId; + // Use the v1testId to extract the VoiceOver assertions column for this task from the v2testRows. It contains a space-delimited set of assertionIds. + // Note that the v1testId was stored in the presentationNumber property in v2testRows. + const voiceOverAssertions = v2testRows.find( + row => row.presentationNumber === voiceOverV1TestId + ).assertions; + // Get array of v1testIds for JAWS and NVDA for this task. + const jawsAndNvdaV1testIds = allInputCsvData.v1tests + .filter(row => row.task === task && row.appliesTo.toLowerCase().search(/jaws|nvda/) >= 0) + .map(row => row.testId); + for (const v1testId of jawsAndNvdaV1testIds) { + const jawsAndNvdaAssertions = v2testRows + .find(row => row.presentationNumber === v1testId) + .assertions.split(' '); + let loggedWarnings = ''; + for (const assertion of jawsAndNvdaAssertions) { + if (!voiceOverAssertions.includes(assertion)) { + if (!loggedWarnings.includes(assertion)) { + const v2testId = v2testRows.find(row => row.presentationNumber === v1testId).testId; + const msg = + `Warning! In ${csvInputFiles.get( + 'v1tests' + )}, task ${task} specified assertionId ${assertion} for JAWS and NVDA but not VoiceOver.\n` + + `${assertion} has been left out of ${testsFile} for testId ${v2testId}. Make sure ${assertion} has the same meaning as another assertion that is specified for ${v2testId}.`; + await util.logMessage(msg); + } + } + } + } + } + + // Now filter v2testRows down to just the VoiceOver rows. + // First, get an array of just the voiceOver rows from v1tests. + const v1voiceOverRows = allInputCsvData.v1tests.filter(row => + row.appliesTo.toLowerCase().includes('voiceover') + ); + // Build a set of the v1 testIds for the VoiceOver tests. + const voiceOverV1testIds = new Set(v1voiceOverRows.map(row => row.testId)); + // Filter v2testRows. Note that the presentationNumber property contains the v1testId. + v2testRows = v2testRows.filter(row => voiceOverV1testIds.has(row.presentationNumber)); + + // Write rows to CSV file + const rowWriter = createCsvWriter({ + path: testsFile, + header: Object.keys(v2testRows[0]).map(column => ({ id: column, title: column })), + }); + await rowWriter.writeRecords(v2testRows); + await util.logMessage(`Wrote ${v2testRows.length} tests to ${path.basename(testsFile)}`); + + return v1ToV2testIds; +} + +async function makeCommandsCsvData(allInputCsvData, v1ToV2testIds) { + /* + The V1 columns are: + testId,task,mode,at,commandA,commandB,commandC... + The V2 columns are: + testId,command,settings,assertionExceptions,presentationNumber + */ + + const v1commandRows = allInputCsvData['v1commands']; + const screenReaders = ['jaws', 'nvda', 'voiceover']; + for (const screenReader of screenReaders) { + // set output file + let outputFile = ''; + let readingModeSetting = ''; + let interactionModeSetting = ''; + switch (screenReader) { + case 'jaws': + outputFile = jawsCommandsFile; + readingModeSetting = 'virtualCursor'; + interactionModeSetting = 'pcCursor'; + break; + case 'nvda': + outputFile = nvdaCommandsFile; + readingModeSetting = 'browseMode'; + interactionModeSetting = 'focusMode'; + break; + case 'voiceover': + outputFile = voCommandsFile; + readingModeSetting = 'quickNavOn'; + break; + } + await util.logMessage(`\nCreating ${path.basename(outputFile)}.`); + + // Get the set of v1commandRows for this screenReader. + let screenReaderV1commandRows = v1commandRows.filter(row => + row.at.toLowerCase().includes(screenReader) + ); + let v2commandRows = []; + for (const row of screenReaderV1commandRows) { + // Make an array of the commands from the columns for commandA, commandB, etc. + let commands = []; + const colNames = Object.keys(row); + for (const colName of colNames) { + if (colName.toLowerCase().startsWith('command')) { + if (row[colName].trim() !== '') commands.push(row[colName]); + } + } + + // Make a new row for each command. + for (const [index, command] of commands.entries()) { + let newRow = {}; + newRow.testId = v1ToV2testIds.get(row.testId); + const { commandSequence, commandSettings } = util.translateCommand( + screenReader, + command, + allInputCsvData.commandSubstitutions + ); + newRow.command = commandSequence; + if (commandSettings.trim() !== '') newRow.settings = commandSettings; + else if (row.mode.toLowerCase().includes('reading')) newRow.settings = readingModeSetting; + else if (row.mode.toLowerCase().includes('interaction')) + newRow.settings = interactionModeSetting; + else newRow.settings = ''; + newRow.assertionExceptions = ''; + newRow.presentationNumber = (Number(row.testId) + 0.1 * index).toFixed(1); + v2commandRows.push(newRow); + } + } + + // Write rows to CSV file + const rowWriter = createCsvWriter({ + path: outputFile, + header: Object.keys(v2commandRows[0]).map(column => ({ id: column, title: column })), + }); + await rowWriter.writeRecords(v2commandRows); + await util.logMessage(`Wrote ${v2commandRows.length} commands to ${path.basename(outputFile)}`); + } +} + +async function makeScriptsCsvData(allInputCsvData) { + await util.logMessage(`\nCreating ${path.basename(scriptsFile)}.`); + + // pull data from the setupScript and setupScriptDescription columns of the v1tests array in allInputCsvData. + let scriptRows = []; + for (const row of allInputCsvData.v1tests) { + // properties in the row objects match columns in the input file: + // testId,title,appliesTo,mode,task,setupScript,setupScriptDescription,refs,instructions,assertion1,assertion2,assertion3,assertion4,assertion5,assertion6,assertion7 + let newRow = {}; + newRow.setupScript = row.setupScript; + newRow.setupScriptDescription = row.setupScriptDescription; + scriptRows.push(newRow); + } + + // Get rid of duplicates. + scriptRows = util.removeDuplicateAndBlankRows(scriptRows); + + // Sort rows by script ascending + scriptRows.sort((a, b) => { + const valueA = a.setupScript; + const valueB = b.setupScript; + if (valueA < valueB) { + return -1; + } + if (valueA > valueB) { + return 1; + } + return 0; + }); + + // Write rows to CSV file + const rowWriter = createCsvWriter({ + path: scriptsFile, + header: Object.keys(scriptRows[0]).map(column => ({ id: column, title: column })), + }); + await rowWriter.writeRecords(scriptRows); + await util.logMessage( + `Wrote ${scriptRows.length} script names and descriptions to ${path.basename(scriptsFile)}` + ); +} + +async function makeReferencesCsvData(allInputCsvData) { + await util.logMessage(`\nCreating ${path.basename(referencesFile)}.`); + + // pull data from the refId and value columns of the v1references aray. + let refRows = []; + for (const row of allInputCsvData.v1references) { + // properties in the row objects match columns in the input file: + // refId, value + let newRow = {}; + newRow.refId = row.refId; + if (row.refId.trim().search(/^author$/i) >= 0) { + newRow.type = 'metadata'; + newRow.value = row.value; + newRow.linkText = ''; + } else if (row.refId.trim().search(/^authorEmail$/i) >= 0) { + newRow.type = 'metadata'; + newRow.value = row.value; + newRow.linkText = ''; + } else if (row.refId.trim().search(/^title$/i) >= 0) { + newRow.type = 'metadata'; + newRow.value = row.value; + newRow.linkText = ''; + } else if (row.refId.trim().search(/^reference$/i) >= 0) { + newRow.type = 'metadata'; + newRow.value = row.value; + const planTitle = allInputCsvData.v1references.find( + row => row.refId.trim().search(/^title$/i) >= 0 + ).value; + newRow.linkText = `Test Case Page for ${planTitle}`; + } else if (row.refId.trim().search(/^example$/i) >= 0) { + newRow.type = 'metadata'; + newRow.value = row.value; + newRow.linkText = ''; + // See if we can update value and link text to represent new APG + if (row.value.toLowerCase().includes('github')) { + let newValue = 'New URL is Required'; + const indexOfDotIo = row.value.toLowerCase().indexOf('.io'); + if (indexOfDotIo >= 0) { + const oldUrlPath = row.value.substring(indexOfDotIo + 3); + newRow.value = v2json.oldToNewAPGExamples[oldUrlPath]; + const indexOfSlashApg = newRow.value.indexOf('/apg'); + if (indexOfSlashApg >= 0) { + const newUrlPath = newRow.value.substring(indexOfSlashApg + 4); + newRow.linkText = `APG Example: ${v2json.apgExamples[newUrlPath]}`; + } + } + } + } else if (row.refId.trim().search(/^designPattern$/i) >= 0) { + newRow.type = 'metadata'; + newRow.value = row.value; + newRow.linkText = 'APG Design Pattern'; + // See if we can update value and link text to represent new APG + if (row.value.toLowerCase().includes('github')) { + let newValue = 'New URL is Required'; + const indexOfHash = row.value.toLowerCase().indexOf('#'); + if (indexOfHash >= 0) { + const oldUrlPath = row.value.substring(indexOfHash); + newRow.value = v2json.oldToNewAPGPatterns[oldUrlPath]; + const indexOfSlashApg = newRow.value.indexOf('/apg'); + if (indexOfSlashApg >= 0) { + const newUrlPath = newRow.value.substring(indexOfSlashApg + 4); + newRow.linkText = v2json.apgPatterns[newUrlPath]; + } + } + } + } else if (row.refId.trim().search(/^developmentDocumentation$/i) >= 0) { + newRow.type = 'metadata'; + newRow.value = row.value; + newRow.linkText = `Development Documentation for ${row.title} Test Plan`; + } else { + newRow.type = 'aria'; + newRow.value = row.refId; + newRow.linkText = row.refId; + } + refRows.push(newRow); + } + + // Write rows to CSV file + const rowWriter = createCsvWriter({ + path: referencesFile, + header: Object.keys(refRows[0]).map(column => ({ id: column, title: column })), + }); + await rowWriter.writeRecords(refRows); + await util.logMessage(`Wrote ${refRows.length} references to ${path.basename(referencesFile)}`); +} diff --git a/scripts/v2maker.json b/scripts/v2maker.json new file mode 100644 index 000000000..f23670121 --- /dev/null +++ b/scripts/v2maker.json @@ -0,0 +1,158 @@ +{ + "oldToNewAPGExamples": { + "/aria-practices/examples/alert/alert.html": "https://www.w3.org/WAI/ARIA/apg/patterns/alert/examples/alert/", + "/aria-practices/examples/breadcrumb/index.html": "https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/examples/breadcrumb/", + "/aria-practices/examples/button/button.html": "https://www.w3.org/WAI/ARIA/apg/patterns/button/examples/button/", + "/aria-practices/examples/checkbox/checkbox-1/checkbox-1.html": "https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/examples/checkbox/", + "/aria-practices/examples/checkbox/checkbox-mixed.html": "https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/examples/checkbox-mixed/", + "/aria-practices/examples/combobox/combobox-autocomplete-both.html": "https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-both/", + "/aria-practices/examples/combobox/combobox-select-only.html": "https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/", + "/aria-practices/examples/dialog-modal/dialog.html": "https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/", + "/aria-practices/examples/disclosure/disclosure-faq.html": "https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-faq/", + "/aria-practices/examples/disclosure/disclosure-navigation.html": "https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/", + "/aria-practices/examples/grid/dataGrids.html": "https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/data-grids/", + "/aria-practices/examples/landmarks/banner.html": "https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/banner.html", + "/aria-practices/examples/landmarks/complementary.html": "https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/complementary.html", + "/aria-practices/examples/landmarks/contentinfo.html": "https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/contentinfo.html", + "/aria-practices/examples/landmarks/form.html": "https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/form.html", + "/aria-practices/examples/landmarks/main.html": "https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/main.html", + "/aria-practices/examples/link/link.html": "https://www.w3.org/WAI/ARIA/apg/patterns/link/examples/link/", + "/aria-practices/examples/menubar/menubar-editor.html": "https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-editor/", + "/aria-practices/examples/menu-button/menu-button-actions.html": "https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-actions/", + "/aria-practices/examples/menu-button/menu-button-actions-active-descendant.html": "https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-actions-active-descendant/", + "/aria-practices/examples/menu-button/menu-button-links.html": "https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-links/", + "/aria-practices/examples/meter/meter.html": "https://www.w3.org/WAI/ARIA/apg/patterns/meter/examples/meter/", + "/aria-practices/examples/radio/radio.html": "https://www.w3.org/WAI/ARIA/apg/patterns/radio/examples/radio/", + "/aria-practices/examples/radio/radio-activedescendant.html": "https://www.w3.org/WAI/ARIA/apg/patterns/radio/examples/radio-activedescendant/", + "/aria-practices/examples/slider/slider-color-viewer.html": "https://www.w3.org/WAI/ARIA/apg/patterns/slider/examples/slider-color-viewer/", + "/aria-practices/examples/slider/slider-rating.html": "https://www.w3.org/WAI/ARIA/apg/patterns/slider/examples/slider-rating/", + "/aria-practices/examples/slider/slider-seek.html": "https://www.w3.org/WAI/ARIA/apg/patterns/slider/examples/slider-seek/", + "/aria-practices/examples/slider/slider-temperature.html": "https://www.w3.org/WAI/ARIA/apg/patterns/slider/examples/slider-temperature/", + "/aria-practices/examples/spinbutton/datepicker-spinbuttons.html": "https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/examples/datepicker-spinbuttons/", + "/aria-practices/examples/switch/switch.html": "https://www.w3.org/WAI/ARIA/apg/patterns/switch/examples/switch/", + "/aria-practices/examples/tabs/tabs-2/tabs.html": "https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/" + }, + "apgExamples": { + "/patterns/accordion/examples/accordion/": "Accordion", + "/patterns/alert/examples/alert/": "Alert", + "/patterns/alertdialog/examples/alertdialog/": "Alert Dialog", + "/patterns/breadcrumb/examples/breadcrumb/": "Breadcrumb", + "/patterns/button/examples/button_idl/": "Button (IDL Version)", + "/patterns/button/examples/button/": "Button", + "/patterns/carousel/examples/carousel-1-prev-next/": "Auto-Rotating Image Carousel with Buttons for Slide Control", + "/patterns/carousel/examples/carousel-2-tablist/": "Auto-Rotating Image Carousel with Tabs for Slide Control", + "/patterns/checkbox/examples/checkbox-mixed/": "Checkbox (Mixed-State)", + "/patterns/checkbox/examples/checkbox/": "Checkbox (Two State)", + "/patterns/combobox/examples/combobox-autocomplete-both/": "Editable Combobox With Both List and Inline Autocomplete", + "/patterns/combobox/examples/combobox-autocomplete-list/": "Editable Combobox With List Autocomplete", + "/patterns/combobox/examples/combobox-autocomplete-none/": "Editable Combobox without Autocomplete", + "/patterns/combobox/examples/combobox-datepicker/": "Date Picker Combobox", + "/patterns/combobox/examples/combobox-select-only/": "Select-Only Combobox", + "/patterns/combobox/examples/grid-combo/": "Editable Combobox with Grid Popup", + "/patterns/dialog-modal/examples/datepicker-dialog/": "Date Picker Dialog", + "/patterns/dialog-modal/examples/dialog/": "Modal Dialog", + "/patterns/disclosure/examples/disclosure-faq/": "Disclosure (Show/Hide) for Answers to Frequently Asked Questions", + "/patterns/disclosure/examples/disclosure-image-description/": "Disclosure (Show/Hide) for Image Description", + "/patterns/disclosure/examples/disclosure-navigation-hybrid/": "Disclosure Navigation Menu with Top-Level Links", + "/patterns/disclosure/examples/disclosure-navigation/": "Disclosure Navigation Menu", + "/patterns/feed/examples/feed/": "Feed", + "/patterns/grid/examples/data-grids/": "Data Grid", + "/patterns/grid/examples/layout-grids/": "Layout Grid", + "/patterns/landmarks/examples/banner.html": "Banner Landmark", + "/patterns/landmarks/examples/complementary.html": "Complementary Landmark", + "/patterns/landmarks/examples/contentinfo.html": "Contentinfo Landmark", + "/patterns/landmarks/examples/form.html": "Form Landmark", + "/patterns/landmarks/examples/main.html": "Main Landmark", + "/patterns/landmarks/examples/navigation.html": "Navigation Landmark", + "/patterns/landmarks/examples/region.html": "Region Landmark", + "/patterns/landmarks/examples/search.html": "Search Landmark", + "/patterns/link/examples/link/": "Link", + "/patterns/listbox/examples/listbox-collapsible/": "(Deprecated) Collapsible Dropdown Listbox", + "/patterns/listbox/examples/listbox-grouped/": "Listbox with Grouped Options", + "/patterns/listbox/examples/listbox-rearrangeable/": "Listboxes with Rearrangeable Options", + "/patterns/listbox/examples/listbox-scrollable/": "Scrollable Listbox", + "/patterns/menu-button/examples/menu-button-actions-active-descendant/": "Actions Menu Button Using aria-activedescendant", + "/patterns/menu-button/examples/menu-button-actions/": "Actions Menu Button Using element.focus()", + "/patterns/menu-button/examples/menu-button-links/": "Navigation Menu Button", + "/patterns/menubar/examples/menubar-editor/": "Editor Menubar", + "/patterns/menubar/examples/menubar-navigation/": "Navigation Menubar", + "/patterns/meter/examples/meter/": "Meter", + "/patterns/radio/examples/radio-activedescendant/": "Radio Group Using aria-activedescendant", + "/patterns/radio/examples/radio-rating/": "Rating Radio Group", + "/patterns/radio/examples/radio/": "Radio Group Using Roving tabindex", + "/patterns/slider-multithumb/examples/slider-multithumb/": "Horizontal Multi-Thumb Slider", + "/patterns/slider/examples/slider-color-viewer/": "Color Viewer Slider", + "/patterns/slider/examples/slider-rating/": "Rating Slider", + "/patterns/slider/examples/slider-seek/": "Media Seek Slider", + "/patterns/slider/examples/slider-temperature/": "Vertical Temperature Slider", + "/patterns/spinbutton/examples/datepicker-spinbuttons/": "Date Picker Spin Button", + "/patterns/switch/examples/switch-button/": "Switch Using HTML Button", + "/patterns/switch/examples/switch-checkbox/": "Switch Using HTML Checkbox Input", + "/patterns/switch/examples/switch/": "Switch", + "/patterns/table/examples/sortable-table/": "Sortable Table", + "/patterns/table/examples/table/": "Table", + "/patterns/tabs/examples/tabs-automatic/": "Tabs with Automatic Activation", + "/patterns/tabs/examples/tabs-manual/": "Tabs with Manual Activation", + "/patterns/toolbar/examples/toolbar/": "Toolbar", + "/patterns/treegrid/examples/treegrid-1/": "Treegrid Email Inbox", + "/patterns/treeview/examples/treeview-1a/": "File Directory Treeview Using Computed Properties", + "/patterns/treeview/examples/treeview-1b/": "File Directory Treeview Using Declared Properties", + "/patterns/treeview/examples/treeview-navigation/": "Navigation Treeview" + }, + "oldToNewAPGPatterns": { + "#alert": "https://www.w3.org/WAI/ARIA/apg/patterns/alert/", + "#aria_lh_banner": "https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/", + "#aria_lh_complementary": "https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/", + "#aria_lh_contentinfo": "https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/", + "#aria_lh_form": "https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/", + "#aria_lh_main": "https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/", + "#breadcrumb": "https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/", + "#button": "https://www.w3.org/WAI/ARIA/apg/patterns/button/", + "#checkbox": "https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/", + "#combobox": "https://www.w3.org/WAI/ARIA/apg/patterns/combobox/", + "#dialog_modal": "https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/", + "#disclosure": "https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/", + "#grid": "https://www.w3.org/WAI/ARIA/apg/patterns/grid/", + "#link": "https://www.w3.org/WAI/ARIA/apg/patterns/link/", + "#menu": "https://www.w3.org/WAI/ARIA/apg/patterns/menubar/", + "#menubutton": "https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/", + "#meter": "https://www.w3.org/WAI/ARIA/apg/patterns/meter/", + "#radiobutton": "https://www.w3.org/WAI/ARIA/apg/patterns/radio/", + "#slider": "https://www.w3.org/WAI/ARIA/apg/patterns/slider/", + "#spinbutton": "https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/", + "#switch": "https://www.w3.org/WAI/ARIA/apg/patterns/switch/", + "#tabpanel": "https://www.w3.org/WAI/ARIA/apg/patterns/tabs/" + }, + "apgPatterns": { + "/patterns/accordion/": "APG Pattern: Accordion (Sections With Show/Hide Functionality)", + "/patterns/alert/": "APG Pattern: Alert", + "/patterns/alertdialog/": "APG Pattern: Alert and Message Dialogs", + "/patterns/breadcrumb/": "APG Pattern: Breadcrumb", + "/patterns/button/": "APG Pattern: Button", + "/patterns/carousel/": "APG Pattern: Carousel (Slide Show or Image Rotator)", + "/patterns/checkbox/": "APG Pattern: Checkbox", + "/patterns/combobox/": "APG Pattern: Combobox", + "/patterns/dialog-modal/": "APG Pattern: Dialog (Modal)", + "/patterns/disclosure/": "APG Pattern: Disclosure (Show/Hide)", + "/patterns/feed/": "APG Pattern: Feed", + "/patterns/grid/": "APG Pattern: Grid (Interactive Tabular Data and Layout Containers)", + "/patterns/landmarks/": "APG Pattern: Landmarks", + "/patterns/link/": "APG Pattern: Link", + "/patterns/listbox/": "APG Pattern: Listbox", + "/patterns/menubar/": "APG Pattern: Menu and Menubar", + "/patterns/menu-button/": "APG Pattern: Menu Button", + "/patterns/meter/": "APG Pattern: Meter", + "/patterns/radio/": "APG Pattern: Radio Group", + "/patterns/slider/": "APG Pattern: Slider", + "/patterns/slider-multithumb/": "APG Pattern: Slider (Multi-Thumb)", + "/patterns/spinbutton/": "APG Pattern: Spinbutton", + "/patterns/switch/": "APG Pattern: Switch", + "/patterns/table/": "APG Pattern: Table", + "/patterns/tabs/": "APG Pattern: Tabs", + "/patterns/toolbar/": "APG Pattern: Toolbar", + "/patterns/tooltip/": "APG Pattern: Tooltip", + "/patterns/treeview/": "APG Pattern: Tree View", + "/patterns/treegrid/": "APG Pattern: Treegrid", + "/patterns/windowsplitter/": "APG Pattern: Window Splitter" + } +} \ No newline at end of file diff --git a/scripts/v2makerUtil.js b/scripts/v2makerUtil.js new file mode 100644 index 000000000..00742de8b --- /dev/null +++ b/scripts/v2makerUtil.js @@ -0,0 +1,271 @@ +const fs = require('fs'); +let logFile = null; +const validLogBehaviors = ['none', 'console', 'file', 'both']; +let logBehavior = 'both'; + +const escapeRegExp = string => { + return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // Escape special characters +}; + +async function deleteFile(filePath) { + if (fs.existsSync(filePath)) { + await fs.unlinkSync(filePath); + } +} + +async function startLoggingTo(name, behavior) { + if (behavior === undefined) logBehavior = 'both'; + if (validLogBehaviors.includes(behavior)) logBehavior = behavior; + else + throw new Error( + `'${behavior}' is not a valid log behavior. Valid behaviors are ${validLogBehaviors}` + ); + if (name === undefined) logFile = './log.txt'; + else logFile = name; + if (logBehavior !== 'none') { + if (logBehavior === 'file' || logBehavior === 'both') await deleteFile(logFile); + const timestamp = new Date().toISOString(); + await logMessage(`Starting format conversion at ${timestamp}`); + } +} + +async function logMessage(message) { + if (logBehavior === 'console' || logBehavior === 'both') console.log(message); + if (logBehavior === 'file' || logBehavior === 'both') { + const logEntry = `${message}\n`; + await fs.appendFile(logFile, logEntry, err => { + if (err) { + throw new Error(`Error writing to ${logFilePath}: ${err}`); + } + }); + } +} + +function setLogBehavior(behavior) { + if (validLogBehaviors.includes(behavior)) logBehavior = behavior; + else + throw new Error( + `'${behavior}' is not a valid log behavior. Valid behaviors are ${validLogBehaviors}` + ); +} + +function getSubstitutionsAndDeletions( + substitutionsFileDescriptor, + csvData, + searchStringsColName, + replacementStringsColName +) { + const substitutionsCsvData = csvData[substitutionsFileDescriptor]; + const substitutions = new Map(); + const deletions = new Set(); + + let headersChecked = false; + for (const row of substitutionsCsvData) { + if (!headersChecked) { + if (!(searchStringsColName in row) || !(replacementStringsColName in row)) { + throw new Error( + `Columns '${searchStringsColName}' and '${replacementStringsColName}' do not exist in the CSV file for ${substitutionsFileDescriptor}.` + ); + } + headersChecked = true; + } + + const oldWords = row[searchStringsColName] || ''; + const newWords = row[replacementStringsColName] || ''; + if (newWords.trim() === '') { + deletions.add(oldWords); + } else { + substitutions.set(oldWords, newWords); + } + } + return { deletions: deletions, substitutions: substitutions }; +} + +function makeId(id, substitutions, deletions) { + // First step is to make substitutions specified in the substitutions file. + + // Make substitutions that replace multiple words + substitutions.forEach((newWord, oldWords) => { + if (oldWords.includes(' ')) { + const regex = new RegExp(`\\b${escapeRegExp(oldWords)}\\b`, 'g'); + id = id.replace(regex, newWord); + } + }); + + // Make substitutions that delete multiple words + deletions.forEach(wordToDelete => { + if (wordToDelete.includes(' ')) { + const regex = new RegExp(`\\b${escapeRegExp(wordToDelete)}\\b`, 'g'); + id = id.replace(regex, ''); + } + }); + + // Make substitutions that replace single words + substitutions.forEach((newWord, oldWords) => { + if (!oldWords.includes(' ')) { + const regex = new RegExp(`\\b${escapeRegExp(oldWords)}\\b`, 'g'); + id = id.replace(regex, newWord); + } + }); + + // Make substitutions that delete single words + deletions.forEach(wordToDelete => { + if (!wordToDelete.includes(' ')) { + const regex = new RegExp(`\\b${escapeRegExp(wordToDelete)}\\b`, 'g'); + id = id.replace(regex, ''); + } + }); + + // step 2 is to Transform to single camel case word by upper casing first letters, removing spaces, and lowercasing start of string. + id = id + .toLowerCase() + .replace(/[^a-z0-9]/g, ' ') + .replace(/\s+/g, ' ') + .split(' ') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(''); + id = id.charAt(0).toLowerCase() + id.slice(1); + + return id; +} + +function translateCommand(screenReader, commandSequence, substitutions) { + commandSequence = commandSequence.replaceAll(',', ' ').toLowerCase(); + let commandSettings = ''; + // Get substitutions for this screenReader + substitutions = substitutions.filter( + row => row.screenReader.toLowerCase().trim() === screenReader.toLowerCase().trim() + ); + if (substitutions.length > 0) { + const oldCommands = commandSequence.split(' '); + let newCommandSequence = []; + let newCommandSettings = []; + for (const oldCommand of oldCommands) { + let newCommand = oldCommand; + for (const substitution of substitutions) { + oldCommandRegex = new RegExp(`^${substitution.oldCommand}$`, 'i'); + if (oldCommand.match(oldCommandRegex)) { + newCommand = oldCommand.replace(oldCommandRegex, substitution.newCommand); + const newSetting = substitution.settings.trim(); + if (newSetting !== '' && !newCommandSettings.includes(newSetting)) + newCommandSettings.push(newSetting); + break; + } + } + newCommandSequence.push(newCommand); + } + commandSequence = newCommandSequence.join(' '); + if (newCommandSettings.length > 0) commandSettings = newCommandSettings.join(' '); + } + commandSequence = commandSequence.replaceAll('_', '+'); + return { commandSequence, commandSettings }; +} + +function fixCase(input, isSentence) { + let entireInputWrappedInSingleQuotes = false; + let entireInputWrappedInDoubleQuotes = false; + let charInsideSingleQuotes = false; + let charInsideDoubleQuotes = false; + let isStartOfFirstWord = true; + + const result = input.split('').map((char, index, arr) => { + // If the entire string is quoted, ignore the leading and trailing quotes, i.e., continue to change case in the string. + if (index === 0) { + if (char === '"' && arr[arr.length - 1] === '"') { + entireInputWrappedInDoubleQuotes = true; + return char; + } + if (char === "'" && arr[arr.length - 1] === "'") { + entireInputWrappedInSingleQuotes = true; + return char; + } + } + if (index === arr.length - 1) { + if (entireInputWrappedInDoubleQuotes) return char; + if (entireInputWrappedInSingleQuotes) return char; + } + // If the entire string is wrapped in double quotes, only pay attention to single quotes on the inside and vice versa. + if (char === '"') { + if (!entireInputWrappedInDoubleQuotes) charInsideDoubleQuotes = !charInsideDoubleQuotes; + return char; + } + if (char === "'") { + if (!entireInputWrappedInSingleQuotes) charInsideSingleQuotes = !charInsideSingleQuotes; + return char; + } + // now process non-quote characters + // Return any white space + if (char.match(/\s/)) { + isStartOfFirstWord = false; + return char; + } + // Change case for any character not inside of quotes + if (!charInsideSingleQuotes && !charInsideDoubleQuotes) { + if (isStartOfFirstWord && isSentence) { + isStartOfFirstWord = false; + return char.toUpperCase(); + } else { + isStartOfFirstWord = false; + return char.toLowerCase(); + } + } else { + return char; + } + }); + + return result.join(''); +} + +function sentenceCase(input) { + return fixCase(input, true); +} + +function phraseCase(input) { + return fixCase(input, false); +} + +function removeDuplicateAndBlankRows(rows) { + // Find and remove rows where all values in all columns are equivalent. + // Also remove blank rows. + const uniqueNonBlankRows = []; + const seenRows = new Set(); + for (const row of rows) { + let blankCellCount = 0; + const rowString = JSON.stringify(row); + if (!seenRows.has(rowString)) { + seenRows.add(rowString); + for (const [key, value] of Object.entries(row)) { + if (value.trim() === '') blankCellCount++; + else break; + } + if (blankCellCount < Object.keys(row).length) { + uniqueNonBlankRows.push(row); + } + } + } + return uniqueNonBlankRows; +} + +function trimQuotes(input) { + // Trim leading and/or trailing double quotes from a string + if (input.length >= 2 && input.charAt(0) === '"') { + input = input.slice(1); + } + if (input.length >= 1 && input.charAt(input.length - 1) === '"') { + input = input.slice(0, -1); + } + return input; +} + +module.exports = { + deleteFile, + startLoggingTo, + logMessage, + getSubstitutionsAndDeletions, + makeId, + translateCommand, + phraseCase, + sentenceCase, + removeDuplicateAndBlankRows, + trimQuotes, +}; diff --git a/scripts/v2substitutionsForAssertionIds.csv b/scripts/v2substitutionsForAssertionIds.csv new file mode 100644 index 000000000..725a33d7e --- /dev/null +++ b/scripts/v2substitutionsForAssertionIds.csv @@ -0,0 +1,12 @@ +oldWords,newWords +a, +an, +backwards,back +Change in state,state change +in, +information,info +is conveyed, +of, +previous,prev +radio button,radio +the, diff --git a/scripts/v2substitutionsForCommands.csv b/scripts/v2substitutionsForCommands.csv new file mode 100644 index 000000000..8d9660b66 --- /dev/null +++ b/scripts/v2substitutionsForCommands.csv @@ -0,0 +1,7 @@ +screenReader,oldCommand,newCommand,settings +VoiceOver,CTRL_OPT_CMD_(\w),$1 ,quickNavOn +VoiceOver,SHIFT_CTRL_OPT_CMD_(\w),$1 ,quickNavOn +VoiceOver,LEFT,LEFT,quickNavOff +VoiceOver,RIGHT,RIGHT,quickNavOff +VoiceOver,DOWN,DOWN,quickNavOff +VoiceOver,UP,UP,quickNavOff diff --git a/scripts/v2substitutionsForTestIds.csv b/scripts/v2substitutionsForTestIds.csv new file mode 100644 index 000000000..1d53a156d --- /dev/null +++ b/scripts/v2substitutionsForTestIds.csv @@ -0,0 +1,17 @@ +oldWords,newWords +a, +an, +backwards,back +in, +in a group, +information,info +interaction, +is conveyed, +mode, +Navigate,nav +of, +previous,prev +radio button,radio +Read,req +reading, +the,