aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorcel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519>2019-12-14 09:16:31 -1000
committercel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519>2019-12-14 09:16:31 -1000
commitde6d39cedeb4af0e54ca04583ad3fe33e2ef094a (patch)
treed28125d512c6fc331fa1fedc9efad0c5aecc9710 /lib
parent0bd830cf4ebf9cf3e4df1f2dba4b851b9361fc14 (diff)
downloadpatchfoo-de6d39cedeb4af0e54ca04583ad3fe33e2ef094a.tar.gz
patchfoo-de6d39cedeb4af0e54ca04583ad3fe33e2ef094a.zip
Sanitize strings
Calling String on an object with a toString property would throw an error
Diffstat (limited to 'lib')
-rw-r--r--lib/render-msg.js152
-rw-r--r--lib/render.js2
-rw-r--r--lib/util.js10
3 files changed, 87 insertions, 77 deletions
diff --git a/lib/render-msg.js b/lib/render-msg.js
index 277b419..4b8c8c0 100644
--- a/lib/render-msg.js
+++ b/lib/render-msg.js
@@ -124,7 +124,7 @@ RenderMsg.prototype.wrap = function (content, cb) {
var ts = this.msg.value.timestamp
var date = ts ? new Date(ts) : null
var self = this
- var channel = this.c.channel ? '#' + this.c.channel : ''
+ var channel = this.c.channel ? '#' + u.toString(this.c.channel) : ''
var done = multicb({pluck: 1, spread: true})
done()(null, [h('tr.msg-row',
h('td.msg-left',
@@ -399,7 +399,7 @@ RenderMsg.prototype.markdown = function (cb) {
RenderMsg.prototype.markdownSource = function (text, mentions) {
return h('div',
- h('pre', String(text)),
+ h('pre', u.toString(text)),
mentions ? [
h('div', h('em', 'mentions:')),
this.valueTable(mentions, 2, function () {})
@@ -478,7 +478,7 @@ RenderMsg.prototype.getName = function (id, cb) {
case '%': return this.getMsgName(id, cb)
case '@': // fallthrough
case '&': return this.getAboutName(id, cb)
- default: return cb(null, String(id))
+ default: return cb(null, u.toString(id))
}
}
@@ -557,7 +557,7 @@ RenderMsg.prototype.title1 = function (cb) {
RenderMsg.prototype.getAboutName = function (id, cb) {
this.app.getAbout(id, function (err, about) {
- cb(err, about && about.name || (String(id).substr(0, 8) + '…'))
+ cb(err, about && about.name || (u.toString(id).substr(0, 8) + '…'))
})
}
@@ -643,7 +643,7 @@ RenderMsg.prototype.about = function (cb) {
isSelf ?
'self-identifies as ' :
['identifies ', h('a', {href: this.toUrl(this.c.about)}, u.truncate(this.c.about, 10)), ' as '],
- h('ins', String(this.c.name))
+ h('ins', u.toString(this.c.name))
], cb)
}
@@ -705,20 +705,20 @@ RenderMsg.prototype.about = function (cb) {
: h('a', {href: this.toUrl(this.c.about)}, u.truncate(this.c.about, 10)),
': '
],
- this.c.name ? [h('ins', String(this.c.name)), ' '] : '',
- this.c.title ? h('h3', String(this.c.title)) : '',
+ this.c.name ? [h('ins', u.toString(this.c.name)), ' '] : '',
+ this.c.title ? h('h3', u.toString(this.c.title)) : '',
this.c.series || this.c.seriesNo ? this.renderSeries(this.c) : '',
- this.c.authors ? h('div', h('em', {title: 'authors'}, String(this.c.authors))) : '',
+ this.c.authors ? h('div', h('em', {title: 'authors'}, u.toString(this.c.authors))) : '',
this.c.rating || this.c.ratingMax || this.c.ratingType ? h('div', [
'rating: ',
- String(this.c.rating),
+ u.toString(this.c.rating),
this.c.ratingMax ? '/' + this.c.ratingMax : '',
this.c.ratingType ? [' ',
h('span', {innerHTML: u.unwrapP(this.render.markdown(this.c.ratingType))})
] : ''
]) : '',
- this.c.genre ? h('div', ['genre: ', h('u', String(this.c.genre))]) : '',
- this.c.shelve ? h('div', ['shelf: ', h('u', String(this.c.shelve))]) : '',
+ this.c.genre ? h('div', ['genre: ', h('u', u.toString(this.c.genre))]) : '',
+ this.c.shelve ? h('div', ['shelf: ', h('u', u.toString(this.c.shelve))]) : '',
this.c.description ? h('div',
{innerHTML: this.render.markdown(this.c.description)}) : '',
this.c.review ? h('blockquote',
@@ -786,7 +786,7 @@ RenderMsg.prototype.contact = function (cb) {
' ', a,
self.c.note ? [
' from ',
- h('code', String(self.c.note))
+ h('code', u.toString(self.c.note))
] : '',
self.c.reason ? [' because ',
h('q', {innerHTML: u.unwrapP(self.render.markdown(self.c.reason))})
@@ -808,7 +808,7 @@ RenderMsg.prototype.pub = function (cb) {
RenderMsg.prototype.address = function (cb) {
var availability = this.c.availability * 100
- var addr = u.extractHostPort(this.value.author, String(this.c.address))
+ var addr = u.extractHostPort(this.value.author, u.toString(this.c.address))
addr = this.app.removeDefaultPort(addr)
this.wrapMini([
'has address',
@@ -832,7 +832,7 @@ RenderMsg.prototype.pubOwnerAnnounce = function (cb) {
RenderMsg.prototype.pubOwnerConfirm = function (cb) {
var self = this
var announcement = this.c.announcement
- var addr = u.extractHostPort(this.value.author, String(this.c.address))
+ var addr = u.extractHostPort(this.value.author, u.toString(this.c.address))
addr = this.app.removeDefaultPort(addr)
this.wrap([
'confirms pub ownership announcement ',
@@ -906,18 +906,18 @@ RenderMsg.prototype.gitRepo = function (cb) {
var name = self.c.name
var upstream = self.c.upstream
self.link(upstream, function (err, upstreamA) {
- if (err) upstreamA = ('a', {href: self.toUrl(upstream)}, String(name))
+ if (err) upstreamA = ('a', {href: self.toUrl(upstream)}, u.toString(name))
self.wrapMini([
upstream ? ['forked ', upstreamA, ': '] : '',
'git clone ',
h('code', h('small', 'ssb://' + id)),
- name ? [' ', h('a', {href: self.toUrl(id)}, String(name))] : ''
+ name ? [' ', h('a', {href: self.toUrl(id)}, u.toString(name))] : ''
], cb)
})
}
function asString(str) {
- return str ? String(str) : str
+ return str ? u.toString(str) : str
}
RenderMsg.prototype.gitUpdate = function (cb) {
@@ -953,8 +953,8 @@ RenderMsg.prototype.gitUpdate = function (cb) {
var path = '/git/commit/' + encodeURIComponent(commit.sha1)
+ '?msg=' + encodeURIComponent(self.msg.key)
return h('li', commit.sha1 ? [h('a', {href: self.render.toUrl(path)},
- h('code', String(commit.sha1).substr(0, 8))), ' '] : '',
- commit.title ? self.linkify(String(commit.title)) : '',
+ h('code', u.toString(commit.sha1).substr(0, 8))), ' '] : '',
+ commit.title ? self.linkify(u.toString(commit.title)) : '',
self.render.gitCommitBody(commit.body)
)
})) : '',
@@ -964,11 +964,11 @@ RenderMsg.prototype.gitUpdate = function (cb) {
+ '?msg=' + encodeURIComponent(self.msg.key)
return h('li',
h('a', {href: self.render.toUrl(path)},
- h('code', String(tag.sha1).substr(0, 8))), ' ',
- 'tagged ', String(tag.type), ' ',
- h('code', String(tag.object).substr(0, 8)), ' ',
- String(tag.tag),
- tag.title ? [': ', self.linkify(String(tag.title).trim()), ' '] : '',
+ h('code', u.toString(tag.sha1).substr(0, 8))), ' ',
+ 'tagged ', u.toString(tag.type), ' ',
+ h('code', u.toString(tag.object).substr(0, 8)), ' ',
+ u.toString(tag.tag),
+ tag.title ? [': ', self.linkify(u.toString(tag.title).trim()), ' '] : '',
tag.body ? self.render.gitCommitBody(tag.body) : ''
)
})) : '',
@@ -991,9 +991,9 @@ RenderMsg.prototype.gitPullRequest = function (cb) {
if (err) return cb(err)
self.wrap(h('div.ssb-pull-request',
'pull request ',
- 'to ', baseRepoLink, ':', String(self.c.branch), ' ',
- 'from ', headRepoLink, ':', String(self.c.head_branch),
- self.c.title ? h('h4', String(self.c.title)) : '',
+ 'to ', baseRepoLink, ':', u.toString(self.c.branch), ' ',
+ 'from ', headRepoLink, ':', u.toString(self.c.head_branch),
+ self.c.title ? h('h4', u.toString(self.c.title)) : '',
h('div', {innerHTML: self.markdown()})), cb)
})
}
@@ -1004,7 +1004,7 @@ RenderMsg.prototype.issue = function (cb) {
if (err) return cb(err)
self.wrap(h('div.ssb-issue',
'issue on ', projectLink,
- self.c.title ? h('h4', String(self.c.title)) : '',
+ self.c.title ? h('h4', u.toString(self.c.title)) : '',
h('div', {innerHTML: self.markdown()})), cb)
})
}
@@ -1082,7 +1082,7 @@ RenderMsg.prototype.valueTable = function (val, depth, cb) {
)
} else if (isContent && key === 'type') {
// TODO: also link to images by type, using links2
- var type = String(val.type)
+ var type = u.toString(val.type)
return h('tr',
h('td', h('strong', 'type')),
h('td', h('a', {href: self.toUrl('/type/' + type)}, type))
@@ -1105,7 +1105,7 @@ RenderMsg.prototype.valueTable = function (val, depth, cb) {
type: 'checkbox', disabled: 'disabled', checked: val ? 'checked' : undefined
})
default:
- return cb(), String(val)
+ return cb(), u.toString(val)
}
}
@@ -1129,8 +1129,8 @@ RenderMsg.prototype.issues = function (cb) {
}
var els = issues.map(function (issue) {
var commit = issue.object || issue.label ? [
- issue.object ? h('code', String(issue.object)) : '', ' ',
- issue.label ? h('q', String(issue.label)) : ''] : ''
+ issue.object ? h('code', u.toString(issue.object)) : '', ' ',
+ issue.label ? h('q', u.toString(issue.label)) : ''] : ''
if (issue.merged === true)
return h('div',
'merged ', self.link1(issue, done()))
@@ -1142,7 +1142,7 @@ RenderMsg.prototype.issues = function (cb) {
'reopened ', self.link1(issue, done()))
if (typeof issue.title === 'string')
return h('div',
- 'renamed ', self.link1(issue, done()), ' to ', h('ins', String(issue.title)))
+ 'renamed ', self.link1(issue, done()), ' to ', h('ins', u.toString(issue.title)))
})
done(cb)
return els.length > 0 ? [els, h('br')] : ''
@@ -1172,11 +1172,11 @@ RenderMsg.prototype.repost = function (cb) {
}
RenderMsg.prototype.update = function (cb) {
- var id = String(this.c.update)
+ var id = u.toString(this.c.update)
this.wrapMini([
h('div', 'updated ', h('code.ssb-id',
h('a', {href: this.render.toUrl(id)}, id))),
- this.c.title ? h('h4.msg-title', String(this.c.title)) : '',
+ this.c.title ? h('h4.msg-title', u.toString(this.c.title)) : '',
this.c.description ? h('div',
{innerHTML: this.render.markdown(this.c.description)}) : ''
], cb)
@@ -1209,7 +1209,7 @@ RenderMsg.prototype.fermentAudio = function (cb) {
: ''),
h('td',
h('a', {href: this.render.toUrl(this.c.audioSrc)},
- String(this.c.title)),
+ u.toString(this.c.title)),
isFinite(this.c.duration)
? ' (' + formatDuration(this.c.duration) + ')'
: '',
@@ -1233,7 +1233,7 @@ RenderMsg.prototype.musicRelease = function (cb) {
}))
: ''),
h('td',
- h('h4.msg-title', String(this.c.title)),
+ h('h4.msg-title', u.toString(this.c.title)),
this.c.text
? h('div', {innerHTML: this.render.markdown(this.c.text)})
: ''
@@ -1242,7 +1242,7 @@ RenderMsg.prototype.musicRelease = function (cb) {
h('ul', u.toArray(this.c.tracks).filter(Boolean).map(function (track) {
return h('li',
h('a', {href: self.render.toUrl(track.link)},
- String(track.fname)))
+ u.toString(track.fname)))
}))
], cb)
}
@@ -1255,10 +1255,10 @@ RenderMsg.prototype.dns = function (cb) {
self.wrap([
h('div',
h('p',
- h('ins', {title: 'name'}, String(record.name)), ' ',
- h('span', {title: 'ttl'}, String(record.ttl)), ' ',
- h('span', {title: 'class'}, String(record.class)), ' ',
- h('span', {title: 'type'}, String(record.type))
+ h('ins', {title: 'name'}, u.toString(record.name)), ' ',
+ h('span', {title: 'ttl'}, u.toString(record.ttl)), ' ',
+ h('span', {title: 'class'}, u.toString(record.class)), ' ',
+ h('span', {title: 'type'}, u.toString(record.type))
),
h('pre', {title: 'data'},
JSON.stringify(record.data || record.value, null, 2)),
@@ -1288,12 +1288,12 @@ RenderMsg.prototype.wifiNetwork = function (cb) {
RenderMsg.prototype.mutualCredit = function (cb) {
var self = this
- var currency = String(self.c.currency)
+ var currency = u.toString(self.c.currency)
self.link(self.c.account, function (err, a) {
if (err) return cb(err)
self.wrapMini([
'credits ', a || '?', ' ',
- h('code', String(self.c.amount)), ' ',
+ h('code', u.toString(self.c.amount)), ' ',
currency[0] === '#'
? h('a', {href: self.toUrl(currency)}, currency)
: h('ins', currency),
@@ -1361,7 +1361,7 @@ RenderMsg.prototype.npmPublish = function (cb) {
self.wrap([
h('div',
'published ',
- h('u', String(pkg.name)), ' ',
+ h('u', u.toString(pkg.name)), ' ',
hJoin(versions.map(function (version) {
var distTag = distTagged[version]
return [h('b', version), distTag ? [' (', h('i', distTag), ')'] : '']
@@ -1473,7 +1473,7 @@ function parseFenRank (line) {
}
function parseChess(fen) {
- var fields = String(fen).split(/\s+/)
+ var fields = u.toString(fen).split(/\s+/)
var ranks = fields[0].split('/')
var f2 = fields[2] || ''
return {
@@ -1644,7 +1644,7 @@ RenderMsg.prototype.chessChat = function (cb) {
if (err) return cb(err)
self.wrap([
h('div', h('small', '> ', rootLink)),
- h('p', String(self.c.msg))
+ h('p', u.toString(self.c.msg))
], cb)
})
}
@@ -1705,7 +1705,7 @@ RenderMsg.prototype.acmeChallengesHttp01 = function (cb) {
href: 'http://' + challenge.domain +
'/.well-known/acme-challenge/' + challenge.token,
title: challenge.keyAuthorization,
- }, String(challenge.domain))
+ }, u.toString(challenge.domain))
}), ', ', ', and ')
), cb)
}
@@ -1732,10 +1732,10 @@ RenderMsg.prototype.bookclub = function (cb) {
}))
})),
h('td',
- h('h4', String(props.title)),
+ h('h4', u.toString(props.title)),
props.series || props.seriesNo ? self.renderSeries(props) : '',
props.authors ?
- h('p', h('em', String(props.authors)))
+ h('p', h('em', u.toString(props.authors)))
: '',
props.description
? h('div', {innerHTML: self.render.markdown(props.description)})
@@ -1773,7 +1773,7 @@ RenderMsg.prototype.sombrioScore = function (cb) {
var self = this
self.wrapMini(h('span',
'scored ',
- h('ins', String(self.c.score))
+ h('ins', u.toString(self.c.score))
), cb)
}
@@ -1794,8 +1794,8 @@ RenderMsg.prototype.blog = function (cb) {
}) : 'blog'),
h('td',
blogId ? h('h3', h('a', {href: self.render.toUrl('/markdown/' + blogId)},
- String(self.c.title || self.msg.key))) : '',
- String(self.c.summary || ''))
+ u.toString(self.c.title || self.msg.key))) : '',
+ u.toString(self.c.summary || ''))
)), cb)
}
@@ -1809,8 +1809,8 @@ RenderMsg.prototype.imageMap = function (cb) {
u.toArray(self.c.areas).map(function (areaLink) {
var href = areaLink && self.toUrl(areaLink.link)
return href ? h('area', {
- shape: String(areaLink.shape),
- coords: String(areaLink.coords),
+ shape: u.toString(areaLink.shape),
+ coords: u.toString(areaLink.coords),
href: href,
}) : ''
})
@@ -1819,7 +1819,7 @@ RenderMsg.prototype.imageMap = function (cb) {
src: self.render.imageUrl(imgRef),
width: Number(imgLink.width) || undefined,
height: Number(imgLink.height) || undefined,
- alt: String(imgLink.name || ''),
+ alt: u.toString(imgLink.name || ''),
usemap: '#' + mapName,
}) : ''
]), cb)
@@ -1829,7 +1829,7 @@ RenderMsg.prototype.skillCreate = function (cb) {
var self = this
self.wrapMini(h('span',
' created skill ',
- h('ins', String(self.c.name))
+ h('ins', u.toString(self.c.name))
), cb)
}
@@ -1861,7 +1861,7 @@ RenderMsg.prototype.identitySkillAssign = function (cb) {
self.wrapMini(h('span',
self.c.action === 'assign' ? 'assigns '
: self.c.action === 'unassign' ? 'unassigns '
- : h('code', String(self.c.action)), ' ',
+ : h('code', u.toString(self.c.action)), ' ',
'skill ', a
), cb)
})
@@ -1876,7 +1876,7 @@ RenderMsg.prototype.ideaSkillAssign = function (cb) {
self.wrapMini(h('span',
self.c.action === 'assign' ? 'assigns '
: self.c.action === 'unassign' ? 'unassigns '
- : h('code', String(self.c.action)), ' ',
+ : h('code', u.toString(self.c.action)), ' ',
'skill ', skillA,
' to idea ',
ideaA
@@ -1890,7 +1890,7 @@ RenderMsg.prototype.ideaAssocate = function (cb) {
self.wrapMini(h('span',
self.c.action === 'associate' ? 'associates with '
: self.c.action === 'disassociate' ? 'disassociates with '
- : h('code', String(self.c.action)), ' ',
+ : h('code', u.toString(self.c.action)), ' ',
'idea ', a
), cb)
})
@@ -1902,7 +1902,7 @@ RenderMsg.prototype.ideaHat = function (cb) {
self.wrapMini(h('span',
self.c.action === 'take' ? 'takes '
: self.c.action === 'discard' ? 'discards '
- : h('code', String(self.c.action)), ' ',
+ : h('code', u.toString(self.c.action)), ' ',
'idea ', a
), cb)
})
@@ -1922,7 +1922,7 @@ RenderMsg.prototype.ideaUpdate = function (cb) {
if (keys === 'title') {
return self.wrapMini(h('span',
'titles idea ',
- h('a', {href: self.toUrl(self.c.ideaKey)}, String(props.title))
+ h('a', {href: self.toUrl(self.c.ideaKey)}, u.toString(props.title))
), cb)
}
@@ -1938,7 +1938,7 @@ RenderMsg.prototype.ideaUpdate = function (cb) {
if (keys === 'description,title') {
return self.wrap(h('div',
'describes idea ',
- h('a', {href: self.toUrl(self.c.ideaKey)}, String(props.title)),
+ h('a', {href: self.toUrl(self.c.ideaKey)}, u.toString(props.title)),
':',
h('blockquote', {innerHTML: self.render.markdown(props.description)})
), cb)
@@ -1974,7 +1974,7 @@ RenderMsg.prototype.aboutResource = function (cb) {
var self = this
return self.wrap(h('div',
'describes resource ',
- h('a', {href: self.toUrl(self.c.about)}, String(self.c.name)),
+ h('a', {href: self.toUrl(self.c.about)}, u.toString(self.c.name)),
':',
h('blockquote', {innerHTML: self.render.markdown(self.c.description)})
), cb)
@@ -1995,11 +1995,11 @@ RenderMsg.prototype.lineComment = function (cb) {
},
updateMsg && updateMsg.value.timestamp
? htime(new Date(updateMsg.value.timestamp))
- : String(self.c.updateId)
+ : u.toString(self.c.updateId)
), ' ',
h('a', {
href: self.toUrl('/git/commit/' + self.c.commitId + '?msg=' + encodeURIComponent(self.c.updateId))
- }, String(self.c.commitId).substr(0, 8)), ' ',
+ }, u.toString(self.c.commitId).substr(0, 8)), ' ',
h('a', {
href: self.toUrl('/git/line-comment/' +
encodeURIComponent(self.msg.key || JSON.stringify(self.msg)))
@@ -2061,16 +2061,16 @@ RenderMsg.prototype.poll = function (cb) {
return h('div',
pollType === 'chooseOne' ? [
h('input', {type: 'radio', name: 'poll_choice', value: i}), ' ',
- String(choice)
+ u.toString(choice)
] : pollType === 'meetingTime' /*|| pollType === 'dot'*/ ? [
h('input', {type: 'checkbox', name: 'poll_choices', value: i}), ' ',
dateSpan(new Date(choice))
/*
] : pollType === 'range' ? [
h('input', {type: 'number', name: 'poll_choice_' + i, min: 0, max: max}), ' ',
- String(choice)
+ u.toString(choice)
*/
- ] : String(choice)
+ ] : u.toString(choice)
)
}),
pollType === 'meetingTime' ? '' : h('p', 'reason: ',
@@ -2119,7 +2119,7 @@ RenderMsg.prototype.pollPosition = function (cb) {
h('ul', pickedChoices.map(function (choice) {
return h('li',
positionType === 'meetingTime' ? dateSpan(new Date(choice))
- : String(choice)
+ : u.toString(choice)
)
})),
reason ? h('div', {innerHTML: self.render.markdown(reason, self.c.mentions)}) : ''
@@ -2153,7 +2153,7 @@ RenderMsg.prototype.pollResolution = function (cb) {
h('ul', pickedChoices.map(function (choice) {
return h('li',
pollType === 'meetingTime' ? dateSpan(new Date(choice))
- : String(choice)
+ : u.toString(choice)
)
}))
), cb)
@@ -2163,7 +2163,7 @@ RenderMsg.prototype.pollResolution = function (cb) {
RenderMsg.prototype.scat = function (cb) {
this.wrapMini([
this.c.action ? '' : 'chats ',
- h('q', String(this.c.text))
+ h('q', u.toString(this.c.text))
], cb)
}
@@ -2174,16 +2174,16 @@ RenderMsg.prototype.share = function (cb) {
self.wrapMini([
'shares ',
share.content === 'blog' ? 'blog '
- : share.content ? [h('code', String(share.content)), ' ']
+ : share.content ? [h('code', u.toString(share.content)), ' ']
: '',
- link || String(share.link),
+ link || u.toString(share.link),
share.url ? [
' at ',
h('small', h('a', {href: self.toUrl(share.url)}, share.url))
] : '',
share.text ? [
': ',
- h('q', String(share.text))
+ h('q', u.toString(share.text))
] : ''
], cb)
})
@@ -2216,7 +2216,7 @@ RenderMsg.prototype.label = function (cb) {
'labeled ',
link,
' as ',
- h('ins', String(self.c.label))
+ h('ins', u.toString(self.c.label))
], cb)
})
}
diff --git a/lib/render.js b/lib/render.js
index 1aec728..e694396 100644
--- a/lib/render.js
+++ b/lib/render.js
@@ -190,7 +190,7 @@ Render.prototype.markdown = function (text, mentions, opts) {
var out = ssbcMd ? md.block(String(text), {
toUrl: function (ref) { return self.toUrl(ref) },
imageLink: function (ref) { return self.imageUrl(ref) }
- }) : marked(String(text), this.markedOpts)
+ }) : marked(u.toString(text), this.markedOpts)
delete this._mentions
delete this._mentionsByLink
return out //fixSymbols(out)
diff --git a/lib/util.js b/lib/util.js
index 731cc82..c5584ee 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -93,6 +93,16 @@ u.toLinkArray = function (x) {
return u.toArray(x).map(u.toLink).filter(u.linkDest)
}
+u.toString = function (str) {
+ switch (typeof str) {
+ case 'string': return str
+ case 'object':
+ if (str !== null) return JSON.stringify(str)
+ default:
+ return String(str)
+ }
+}
+
u.renderError = function(err) {
return h('div.error',
h('h3', err.name),