Created
July 1, 2021 05:15
-
-
Save kissarat/c1ae2d7e325ad2b1a4d771ba7711976e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Converts the given form URL into a JSON object. | |
*/ | |
function main(url = '') { | |
let form = url | |
? FormApp.openByUrl(url) | |
: FormApp.getActiveForm(); | |
let items = form.getItems(); | |
let result = { | |
format: 'quiz/1.0.0', | |
exportedAt: new Date().toISOString(), | |
...getFormMetadata(form), | |
count: items.length, | |
items: items.map(itemToObject), | |
text: vocabulary.serialize() | |
}; | |
Logger.log(JSON.stringify(result, null, ' ')); | |
} | |
const idRadix = 10 | |
const idRank = 4 | |
const numericalField = 'nid' | |
const wordParts = { | |
"can't": 'cannot', | |
"'ll": ' will' | |
} | |
class Vocabulary { | |
constructor(items = [], radix = idRadix, rank = idRank) { | |
this.items = items || [], | |
this.radix = radix || idRadix, | |
this.rank = rank || idRank | |
} | |
find(str, propName = 'id') { | |
return this.items.find(item => str === item[propName]) | |
} | |
add(text, id) { | |
const found = this.find(text, 'text') | |
if (found) { | |
return found.id | |
} | |
if (!id && text.length < 30) { | |
const shortName = text | |
.toLowerCase() | |
.replace(/\w+'\w+/g, (s) => { | |
const key = Object.keys(wordParts).find(key => s.indexOf(key) >= 0) | |
return key | |
? s.replace(key, wordParts[key]) | |
: key | |
}) | |
.replace(/[^a-z]+/g, ' ') | |
.trim() | |
.replace(/ +/g, '_') | |
if (shortName.length > 2) { | |
id = snakeCaseToCamelCase(shortName) | |
id = id[0].toUpperCase() + id.substr(1) | |
} | |
} | |
if (id) { | |
if (!this.find(id, 'id')) { | |
this.items.push({ id, text }) | |
} | |
return id | |
} | |
const lastNumberId = this.items.reduce((acc, item) => { | |
if ('number' === typeof item[numericalField]) { | |
return Math.max(acc, item[numericalField]) | |
} | |
return acc | |
}, 0) | |
const n = lastNumberId + 1 | |
id = n.toString(this.radix) | |
this.items.push({ id, text, [numericalField]: n }) | |
return id | |
} | |
serialize() { | |
return this.items.reduce((acc, item) => { | |
acc[item.id] = item.text | |
return acc | |
}, {}) | |
} | |
} | |
const vocabulary = new Vocabulary() | |
const ref = (text, id) => ('#' + vocabulary.add(text, id)) | |
const messageRef = (text, id) => ref(text, id ? `${id}Message` : undefined) | |
const valueRef = (text) => 'string' === typeof text | |
? ref(text) | |
: text | |
const exportUser = (user = FormApp.getActiveForm().getEditors()[0]) => user.getEmail() | |
// const exportUser = (user = FormApp.getActiveForm().getEditors()[0]) => ({ | |
// googleId: user.getUserLoginId(), | |
// email: user.getEmail() | |
// }) | |
const exportUserList = (users) => users.map(exportUser).sort() | |
/** | |
* Returns the form metadata object for the given Form object. | |
* @param form: Form | |
* @returns (Object) object of form metadata. | |
*/ | |
function getFormMetadata(form) { | |
return { | |
"title": ref(form.getTitle(), 'Title'), | |
"id": form.getId(), | |
"description": ref(form.getDescription(), 'Description'), | |
"publishedUrl": form.getPublishedUrl(), | |
"editorEmails": form.getEditors().map(exportUser), | |
"confirmationMessage": messageRef(form.getConfirmationMessage(), 'Confirmation'), | |
"customClosedFormMessage": messageRef(form.getCustomClosedFormMessage(), 'CustomClosedForm'), | |
}; | |
} | |
/** | |
* Returns an Object for a given Item. | |
* @param item: Item | |
* @returns (Object) object for the given item. | |
*/ | |
function itemToObject(item) { | |
const data = { | |
type: item.getType().toString(), | |
title: ref(item.getTitle()) | |
}; | |
// Downcast items to access type-specific properties | |
const itemTypeConstructorName = snakeCaseToCamelCase(`AS_${data.type}_ITEM`); | |
const typedItem = item[itemTypeConstructorName](); | |
// Keys with a prefix of "get" have "get" stripped | |
const knownKeys = ["image", "choices", "type", "alignment"] | |
const feedbackRelatedGetters = ["getFeedbackForIncorrect", "getFeedbackForCorrect", "getGeneralFeedback"] | |
const booleanGetterPrefixes = ['is', 'has', 'includes'] | |
const getterPrefixes = ['get', ...booleanGetterPrefixes] | |
Object.keys(typedItem) | |
.filter(s => getterPrefixes.some(prefix => s.indexOf(prefix) === 0)) | |
.forEach((getKeyMethodName) => { | |
const propName = getKeyMethodName[3].toLowerCase() + getKeyMethodName.substr(4); | |
const skipKey = propName in data | |
|| knownKeys.includes(propName) | |
|| feedbackRelatedGetters.some(s => s.equals(getKeyMethodName)) | |
if (skipKey) { | |
return | |
}; | |
data[propName] = typedItem[getKeyMethodName](); | |
}); | |
// Handle image data and list choices | |
switch (item.getType()) { | |
case FormApp.ItemType.LIST: | |
case FormApp.ItemType.CHECKBOX: | |
case FormApp.ItemType.MULTIPLE_CHOICE: | |
data.choices = typedItem.getChoices().map(choice => valueRef(choice.getValue())); | |
break; | |
case FormApp.ItemType.IMAGE: | |
data.alignment = typedItem.getAlignment().toString(); | |
if (item.getType() == FormApp.ItemType.VIDEO) { | |
return; | |
} | |
let imageBlob = typedItem.getImage(); | |
data.imageBlob = { | |
"dataAsString": imageBlob.getDataAsString(), | |
"name": imageBlob.getName(), | |
"isGoogleType": imageBlob.isGoogleType() | |
}; | |
break; | |
case FormApp.ItemType.PAGE_BREAK: | |
data.pageNavigationType = typedItem.getPageNavigationType().toString(); | |
break; | |
default: | |
break; | |
} | |
// Have to do this because for some reason Google Scripts API doesn't have a | |
// native VIDEO type | |
if (item.getType().toString() === "VIDEO") { | |
data.alignment = typedItem.getAlignment().toString(); | |
} | |
return data; | |
} | |
/** | |
* Converts a SNAKE_CASE string to a camelCase string. | |
* @param s: string in snake_case | |
* @returns (string) the camelCase version of that string | |
*/ | |
function snakeCaseToCamelCase(s) { | |
return s.toLowerCase().replace(/(\_\w)/g, function(m) {return m[1].toUpperCase();}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment