Skip to content

Instantly share code, notes, and snippets.

@kissarat
Created July 1, 2021 05:15
Show Gist options
  • Save kissarat/c1ae2d7e325ad2b1a4d771ba7711976e to your computer and use it in GitHub Desktop.
Save kissarat/c1ae2d7e325ad2b1a4d771ba7711976e to your computer and use it in GitHub Desktop.
/**
* 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