mirror of https://github.com/docusealco/docuseal
Compare commits
15 Commits
8c4cbd86f7
...
911e55ccc3
| Author | SHA1 | Date |
|---|---|---|
|
|
911e55ccc3 | 3 weeks ago |
|
|
41fedfcc40 | 3 weeks ago |
|
|
2f9fc95af3 | 3 weeks ago |
|
|
11b0b16ca7 | 3 weeks ago |
|
|
f8cb7ffdab | 3 weeks ago |
|
|
8b59c0aaa0 | 4 weeks ago |
|
|
bb2fb7a0c2 | 4 weeks ago |
|
|
8f8b36617a | 4 weeks ago |
|
|
ff53436fd4 | 4 weeks ago |
|
|
cf09a4f733 | 4 weeks ago |
|
|
2303c21cea | 4 weeks ago |
|
|
89e797b95f | 4 weeks ago |
|
|
20375c3a42 | 4 weeks ago |
|
|
339ceda18d | 4 weeks ago |
|
|
97ce32fd52 | 4 weeks ago |
@ -1,92 +1,25 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmissionsDownloadController < ApplicationController
|
class SubmissionsDownloadController < ApplicationController
|
||||||
skip_before_action :authenticate_user!
|
load_and_authorize_resource :submission
|
||||||
skip_authorization_check
|
|
||||||
|
|
||||||
TTL = 40.minutes
|
|
||||||
FILES_TTL = 5.minutes
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
signature_valid =
|
|
||||||
if @submitter&.slug == params[:submitter_slug]
|
|
||||||
true
|
|
||||||
else
|
|
||||||
@submitter = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(@submitter)
|
|
||||||
|
|
||||||
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
|
||||||
|
|
||||||
return head :not_found unless last_submitter
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
if !signature_valid && !current_user_submitter?(last_submitter)
|
|
||||||
return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
|
||||||
|
|
||||||
if last_submitter.completed_at < TTL.ago
|
|
||||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
|
||||||
|
|
||||||
return head :not_found
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if params[:combined] == 'true'
|
if params[:combined] == 'true'
|
||||||
respond_with_combined(last_submitter)
|
url = Submitters.build_combined_url(last_submitter)
|
||||||
else
|
|
||||||
render json: build_urls(last_submitter)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
if url
|
||||||
|
render json: [url]
|
||||||
def respond_with_combined(submitter)
|
else
|
||||||
url = build_combined_url(submitter)
|
head :not_found
|
||||||
|
end
|
||||||
if url
|
|
||||||
render json: [url]
|
|
||||||
else
|
else
|
||||||
head :not_found
|
render json: Submitters.build_document_urls(last_submitter)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_user_submitter?(submitter)
|
|
||||||
current_user && current_ability.can?(:read, submitter)
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_urls(submitter)
|
|
||||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
|
||||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
|
||||||
|
|
||||||
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
|
||||||
ActiveStorage::Blob.proxy_path(
|
|
||||||
attachment.blob,
|
|
||||||
expires_at: FILES_TTL.from_now.to_i,
|
|
||||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_combined_url(submitter)
|
|
||||||
return if submitter.submission.submitters.exists?(completed_at: nil)
|
|
||||||
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
|
||||||
|
|
||||||
attachment = submitter.submission.combined_document_attachment
|
|
||||||
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
|
|
||||||
|
|
||||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
|
||||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
|
||||||
|
|
||||||
ActiveStorage::Blob.proxy_path(
|
|
||||||
attachment.blob,
|
|
||||||
expires_at: FILES_TTL.from_now.to_i,
|
|
||||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@ -0,0 +1,64 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsPreviewDownloadController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
TTL = 40.minutes
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submission = Submission.find_by!(slug: params[:submission_slug] || params[:submissions_preview_slug])
|
||||||
|
|
||||||
|
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
|
unless current_user_submission?(@submission)
|
||||||
|
if use_2fa?(@submission)
|
||||||
|
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
if last_submitter.completed_at < TTL.ago
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:combined] == 'true'
|
||||||
|
respond_with_combined(last_submitter)
|
||||||
|
else
|
||||||
|
render json: Submitters.build_document_urls(last_submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def respond_with_combined(submitter)
|
||||||
|
url = Submitters.build_combined_url(submitter)
|
||||||
|
|
||||||
|
if url
|
||||||
|
render json: [url]
|
||||||
|
else
|
||||||
|
head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user_submission?(submission)
|
||||||
|
current_user && current_ability.can?(:read, submission)
|
||||||
|
end
|
||||||
|
|
||||||
|
def use_2fa?(submission)
|
||||||
|
return true if submission.submitters.any? do |e|
|
||||||
|
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
|
||||||
|
end
|
||||||
|
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||||
|
return true if submission.template&.preferences&.dig('require_email_2fa')
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormCompletedDownloadController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
TTL = 40.minutes
|
||||||
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||||
|
|
||||||
|
signature_valid =
|
||||||
|
if @submitter&.slug == submitter_slug
|
||||||
|
true
|
||||||
|
else
|
||||||
|
@submitter = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
@submitter ||= Submitter.find_by!(slug: submitter_slug)
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
|
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
|
if !signature_valid && !current_user_submitter?(last_submitter)
|
||||||
|
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
if last_submitter.completed_at < TTL.ago
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:combined] == 'true'
|
||||||
|
respond_with_combined(last_submitter)
|
||||||
|
else
|
||||||
|
render json: Submitters.build_document_urls(last_submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submitter_slug
|
||||||
|
params[:submit_form_slug] || params[:submitter_slug] || params[:submitter_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_with_combined(submitter)
|
||||||
|
url = Submitters.build_combined_url(submitter)
|
||||||
|
|
||||||
|
if url
|
||||||
|
render json: [url]
|
||||||
|
else
|
||||||
|
head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user_submitter?(submitter)
|
||||||
|
current_user && current_ability.can?(:read, submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmittersDownloadController < ApplicationController
|
||||||
|
load_and_authorize_resource :submitter
|
||||||
|
|
||||||
|
def index
|
||||||
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
|
render json: Submitters.build_document_urls(@submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,874 @@
|
|||||||
|
/*
|
||||||
|
* Portions of this file are derived from the Dentaku gem
|
||||||
|
* https://github.com/rubysolo/dentaku
|
||||||
|
*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2012 Solomon White
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* Modifications and JavaScript port
|
||||||
|
* Copyright (c) 2026 DocuSeal, LLC
|
||||||
|
*/
|
||||||
|
|
||||||
|
class CalculatorError extends Error { constructor (msg) { super(msg); this.name = 'CalculatorError' } }
|
||||||
|
class ParseError extends CalculatorError { constructor (msg) { super(msg); this.name = 'ParseError' } }
|
||||||
|
class TokenizerError extends CalculatorError { constructor (msg) { super(msg); this.name = 'TokenizerError' } }
|
||||||
|
class ArgumentError extends CalculatorError { constructor (msg) { super(msg); this.name = 'ArgumentError' } }
|
||||||
|
class ZeroDivisionError extends CalculatorError { constructor () { super('divided by 0'); this.name = 'ZeroDivisionError' } }
|
||||||
|
class UnboundVariableError extends CalculatorError {
|
||||||
|
constructor (vars) {
|
||||||
|
super('no value provided for variables: ' + vars.join(', '))
|
||||||
|
this.name = 'UnboundVariableError'
|
||||||
|
this.unbound = vars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Token {
|
||||||
|
constructor (category, value, raw) {
|
||||||
|
this.category = category
|
||||||
|
this.value = value
|
||||||
|
this.raw = raw == null ? String(value) : String(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
is (cat) { return this.category === cat }
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPERATOR_NAMES = {
|
||||||
|
'^': 'pow',
|
||||||
|
'+': 'add',
|
||||||
|
'-': 'subtract',
|
||||||
|
'*': 'multiply',
|
||||||
|
'/': 'divide',
|
||||||
|
'%': 'mod',
|
||||||
|
'|': 'bitor',
|
||||||
|
'&': 'bitand',
|
||||||
|
'<<': 'bitshiftleft',
|
||||||
|
'>>': 'bitshiftright'
|
||||||
|
}
|
||||||
|
const COMPARATOR_NAMES = {
|
||||||
|
'<=': 'le',
|
||||||
|
'>=': 'ge',
|
||||||
|
'!=': 'ne',
|
||||||
|
'<>': 'ne',
|
||||||
|
'<': 'lt',
|
||||||
|
'>': 'gt',
|
||||||
|
'==': 'eq',
|
||||||
|
'=': 'eq'
|
||||||
|
}
|
||||||
|
const GROUPING_NAMES = { '(': 'open', ')': 'close', ',': 'comma' }
|
||||||
|
|
||||||
|
function buildScanners (caseSensitive) {
|
||||||
|
return [
|
||||||
|
(s) => {
|
||||||
|
const m = /^\s+/.exec(s)
|
||||||
|
return m ? { tokens: [], length: m[0].length } : null
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^null\b/i.exec(s)
|
||||||
|
return m ? { tokens: [new Token('null', null, m[0])], length: m[0].length } : null
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^((?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?)\b/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('numeric', Number(m[0]), m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^0x[0-9a-f]+\b/i.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('numeric', parseInt(m[0].slice(2), 16), m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^"([^"]*)"/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('string', m[1], m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^'([^']*)'/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('string', m[1], m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s, last) => {
|
||||||
|
if (s[0] !== '-') return null
|
||||||
|
const ok = last == null ||
|
||||||
|
last.is('operator') ||
|
||||||
|
last.is('comparator') ||
|
||||||
|
last.is('combinator') ||
|
||||||
|
last.value === 'open' ||
|
||||||
|
last.value === 'comma' ||
|
||||||
|
last.value === 'array_start'
|
||||||
|
if (!ok) return null
|
||||||
|
return { tokens: [new Token('operator', 'negate', '-')], length: 1 }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^(and|or|&&|\|\|)\s/i.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
const raw = m[1].toLowerCase()
|
||||||
|
const name = raw === '&&' ? 'and' : raw === '||' ? 'or' : raw
|
||||||
|
return { tokens: [new Token('combinator', name, m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^(<<|>>|\^|\+|-|\*|\/|%|\||&)/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('operator', OPERATOR_NAMES[m[0]], m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^(\(|\)|,)/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('grouping', GROUPING_NAMES[m[0]], m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
if (s[0] === '{') return { tokens: [new Token('array', 'array_start', '{')], length: 1 }
|
||||||
|
if (s[0] === '}') return { tokens: [new Token('array', 'array_end', '}')], length: 1 }
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^(<=|>=|!=|<>|==|=|<|>)/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('comparator', COMPARATOR_NAMES[m[0]], m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^(true|false)\b/i.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('logical', m[0].toLowerCase() === 'true', m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^(\w+!?)\s*\(/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
const name = m[1].toLowerCase()
|
||||||
|
return {
|
||||||
|
tokens: [
|
||||||
|
new Token('function', name, m[1]),
|
||||||
|
new Token('grouping', 'open', '(')
|
||||||
|
],
|
||||||
|
length: m[0].length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^[A-Za-z_][\w.]*\b/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
const value = caseSensitive ? m[0] : m[0].toLowerCase()
|
||||||
|
return { tokens: [new Token('identifier', value, m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^`([^`]*)`/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('identifier', m[1], m[0])], length: m[0].length }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenize (input, options) {
|
||||||
|
options = options || {}
|
||||||
|
let remaining = String(input).replace(/\/\*[\s\S]*?\*\//g, '')
|
||||||
|
const scanners = buildScanners(options.caseSensitive === true)
|
||||||
|
const tokens = []
|
||||||
|
let nesting = 0
|
||||||
|
|
||||||
|
while (remaining.length > 0) {
|
||||||
|
let matched = false
|
||||||
|
for (let i = 0; i < scanners.length; i++) {
|
||||||
|
const result = scanners[i](remaining, tokens.length ? tokens[tokens.length - 1] : null)
|
||||||
|
if (result) {
|
||||||
|
for (const tok of result.tokens) {
|
||||||
|
if (tok.category === 'grouping' && tok.value === 'open') nesting++
|
||||||
|
if (tok.category === 'grouping' && tok.value === 'close') {
|
||||||
|
nesting--
|
||||||
|
if (nesting < 0) throw new TokenizerError('too many closing parentheses')
|
||||||
|
}
|
||||||
|
tokens.push(tok)
|
||||||
|
}
|
||||||
|
remaining = remaining.slice(result.length)
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched) throw new TokenizerError("parse error at: '" + remaining + "'")
|
||||||
|
}
|
||||||
|
if (nesting > 0) throw new TokenizerError('too many opening parentheses')
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniq (arr) {
|
||||||
|
const seen = new Set()
|
||||||
|
const out = []
|
||||||
|
for (const x of arr) { if (!seen.has(x)) { seen.add(x); out.push(x) } }
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumber (val) {
|
||||||
|
if (typeof val === 'number') return val
|
||||||
|
if (typeof val === 'boolean') return val ? 1 : 0
|
||||||
|
if (val == null) throw new ArgumentError("'" + val + "' is not coercible to numeric")
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
if (val.length === 0) throw new ArgumentError("'' is not coercible to numeric")
|
||||||
|
const n = Number(val)
|
||||||
|
if (isNaN(n)) throw new ArgumentError("'" + val + "' is not coercible to numeric")
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
throw new ArgumentError("'" + val + "' is not coercible to numeric")
|
||||||
|
}
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
static precedence = 0
|
||||||
|
static arity = null
|
||||||
|
static rightAssociative = false
|
||||||
|
static resolveClass () { return this }
|
||||||
|
static minParamCount () { return this.arity }
|
||||||
|
static maxParamCount () { return this.arity }
|
||||||
|
value () { return null }
|
||||||
|
dependencies () { return [] }
|
||||||
|
get type () { return null }
|
||||||
|
}
|
||||||
|
|
||||||
|
class NilNode extends Node { value () { return null } }
|
||||||
|
|
||||||
|
class Numeric extends Node {
|
||||||
|
constructor (token) { super(); this.v = token.value }
|
||||||
|
value () { return this.v }
|
||||||
|
get type () { return 'numeric' }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logical extends Node {
|
||||||
|
constructor (token) { super(); this.v = token.value }
|
||||||
|
value () { return this.v }
|
||||||
|
get type () { return 'logical' }
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringNode extends Node {
|
||||||
|
constructor (token) { super(); this.v = token.value }
|
||||||
|
value () { return this.v }
|
||||||
|
get type () { return 'string' }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Identifier extends Node {
|
||||||
|
constructor (token) { super(); this.ident = token.value }
|
||||||
|
value (ctx) {
|
||||||
|
if (ctx && (this.ident in ctx)) {
|
||||||
|
const v = ctx[this.ident]
|
||||||
|
if (v instanceof Node) return v.value(ctx)
|
||||||
|
if (typeof v === 'function') return v()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
throw new UnboundVariableError([this.ident])
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies (ctx) {
|
||||||
|
if (ctx && (this.ident in ctx)) {
|
||||||
|
const v = ctx[this.ident]
|
||||||
|
return v instanceof Node ? v.dependencies(ctx) : []
|
||||||
|
}
|
||||||
|
return [this.ident]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Grouping extends Node {
|
||||||
|
constructor (node) { super(); this.node = node }
|
||||||
|
value (ctx) { return this.node.value(ctx) }
|
||||||
|
get type () { return this.node.type }
|
||||||
|
dependencies (ctx) { return this.node.dependencies(ctx) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Operation extends Node {
|
||||||
|
constructor (left, right) { super(); this.left = left; this.right = right }
|
||||||
|
dependencies (ctx) {
|
||||||
|
const l = this.left ? this.left.dependencies(ctx) : []
|
||||||
|
const r = this.right ? this.right.dependencies(ctx) : []
|
||||||
|
return uniq(l.concat(r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arithmetic extends Operation { get type () { return 'numeric' } }
|
||||||
|
|
||||||
|
class Addition extends Arithmetic {
|
||||||
|
static precedence = 10
|
||||||
|
value (ctx) {
|
||||||
|
const l = this.left.value(ctx)
|
||||||
|
const r = this.right.value(ctx)
|
||||||
|
if (typeof l === 'string' || typeof r === 'string') return String(l) + String(r)
|
||||||
|
return toNumber(l) + toNumber(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Subtraction extends Arithmetic {
|
||||||
|
static precedence = 10
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) - toNumber(this.right.value(ctx)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Multiplication extends Arithmetic {
|
||||||
|
static precedence = 20
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) * toNumber(this.right.value(ctx)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Division extends Arithmetic {
|
||||||
|
static precedence = 20
|
||||||
|
value (ctx) {
|
||||||
|
const r = toNumber(this.right.value(ctx))
|
||||||
|
if (r === 0) throw new ZeroDivisionError()
|
||||||
|
return toNumber(this.left.value(ctx)) / r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Modulo extends Arithmetic {
|
||||||
|
static arity = 2
|
||||||
|
static precedence = 20
|
||||||
|
static resolveClass (nextTok) {
|
||||||
|
if (!nextTok) return Percentage
|
||||||
|
if (nextTok.category === 'operator') return Percentage
|
||||||
|
if (nextTok.category === 'comparator') return Percentage
|
||||||
|
if (nextTok.category === 'combinator') return Percentage
|
||||||
|
if (nextTok.category === 'grouping' && (nextTok.value === 'close' || nextTok.value === 'comma')) return Percentage
|
||||||
|
if (nextTok.category === 'array' && nextTok.value === 'array_end') return Percentage
|
||||||
|
return Modulo
|
||||||
|
}
|
||||||
|
|
||||||
|
value (ctx) {
|
||||||
|
const r = toNumber(this.right.value(ctx))
|
||||||
|
if (r === 0) throw new ZeroDivisionError()
|
||||||
|
return toNumber(this.left.value(ctx)) % r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Percentage extends Arithmetic {
|
||||||
|
static arity = 1
|
||||||
|
static precedence = 30
|
||||||
|
constructor (child) { super(child, null) }
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) * 0.01 }
|
||||||
|
dependencies (ctx) { return this.left.dependencies(ctx) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Exponentiation extends Arithmetic {
|
||||||
|
static precedence = 30
|
||||||
|
static rightAssociative = true
|
||||||
|
value (ctx) { return Math.pow(toNumber(this.left.value(ctx)), toNumber(this.right.value(ctx))) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Negation extends Arithmetic {
|
||||||
|
static arity = 1
|
||||||
|
static precedence = 40
|
||||||
|
static rightAssociative = true
|
||||||
|
constructor (node) { super(node, null) }
|
||||||
|
value (ctx) { return -toNumber(this.left.value(ctx)) }
|
||||||
|
dependencies (ctx) { return this.left.dependencies(ctx) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class BitwiseOr extends Operation {
|
||||||
|
static precedence = 10
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) | toNumber(this.right.value(ctx)) }
|
||||||
|
get type () { return 'numeric' }
|
||||||
|
}
|
||||||
|
class BitwiseAnd extends Operation {
|
||||||
|
static precedence = 10
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) & toNumber(this.right.value(ctx)) }
|
||||||
|
get type () { return 'numeric' }
|
||||||
|
}
|
||||||
|
class BitwiseShiftLeft extends Operation {
|
||||||
|
static precedence = 10
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) << toNumber(this.right.value(ctx)) }
|
||||||
|
get type () { return 'numeric' }
|
||||||
|
}
|
||||||
|
class BitwiseShiftRight extends Operation {
|
||||||
|
static precedence = 10
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) >> toNumber(this.right.value(ctx)) }
|
||||||
|
get type () { return 'numeric' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpCast (v) {
|
||||||
|
if (typeof v === 'string' && /^-?\d*\.?\d+$/.test(v)) return Number(v)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
class Comparator extends Operation {
|
||||||
|
static precedence = 5
|
||||||
|
get type () { return 'logical' }
|
||||||
|
}
|
||||||
|
class LessThan extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) < cmpCast(this.right.value(ctx)) } }
|
||||||
|
class LessThanOrEqual extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) <= cmpCast(this.right.value(ctx)) } }
|
||||||
|
class GreaterThan extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) > cmpCast(this.right.value(ctx)) } }
|
||||||
|
class GreaterThanOrEqual extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) >= cmpCast(this.right.value(ctx)) } }
|
||||||
|
class NotEqual extends Comparator {
|
||||||
|
value (ctx) {
|
||||||
|
const l = cmpCast(this.left.value(ctx))
|
||||||
|
const r = cmpCast(this.right.value(ctx))
|
||||||
|
return l !== r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Equal extends Comparator {
|
||||||
|
value (ctx) {
|
||||||
|
const l = cmpCast(this.left.value(ctx))
|
||||||
|
const r = cmpCast(this.right.value(ctx))
|
||||||
|
return l === r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Combinator extends Operation {
|
||||||
|
static precedence = 0
|
||||||
|
get type () { return 'logical' }
|
||||||
|
}
|
||||||
|
class And extends Combinator { value (ctx) { return Boolean(this.left.value(ctx)) && Boolean(this.right.value(ctx)) } }
|
||||||
|
class Or extends Combinator { value (ctx) { return Boolean(this.left.value(ctx)) || Boolean(this.right.value(ctx)) } }
|
||||||
|
|
||||||
|
class ArrayNode extends Node {
|
||||||
|
static arity = null
|
||||||
|
static minParamCount () { return 0 }
|
||||||
|
static maxParamCount () { return Infinity }
|
||||||
|
constructor (...items) {
|
||||||
|
super()
|
||||||
|
this.items = items
|
||||||
|
}
|
||||||
|
|
||||||
|
value (ctx) { return this.items.map((i) => i.value(ctx)) }
|
||||||
|
dependencies (ctx) {
|
||||||
|
return uniq([].concat.apply([], this.items.map((i) => i.dependencies(ctx))))
|
||||||
|
}
|
||||||
|
|
||||||
|
get type () { return 'array' }
|
||||||
|
}
|
||||||
|
|
||||||
|
class FunctionNode extends Node {
|
||||||
|
static arity = null
|
||||||
|
static minParamCount () { return 0 }
|
||||||
|
static maxParamCount () { return Infinity }
|
||||||
|
constructor (...args) {
|
||||||
|
super()
|
||||||
|
this.args = args
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies (ctx) {
|
||||||
|
return uniq([].concat.apply([], this.args.map((a) => a.dependencies(ctx))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FUNCTION_REGISTRY = Object.create(null)
|
||||||
|
function registerFunctionClass (name, cls) { FUNCTION_REGISTRY[name.toLowerCase()] = cls }
|
||||||
|
function getFunctionClass (name) { return FUNCTION_REGISTRY[name.toLowerCase()] }
|
||||||
|
|
||||||
|
function defineFunction (name, spec) {
|
||||||
|
const min = spec.min == null ? 0 : spec.min
|
||||||
|
const max = spec.max == null ? Infinity : spec.max
|
||||||
|
const call = spec.call
|
||||||
|
const type = spec.type || 'numeric'
|
||||||
|
class F extends FunctionNode {
|
||||||
|
static arity = (min === max) ? min : null
|
||||||
|
static minParamCount () { return min }
|
||||||
|
static maxParamCount () { return max }
|
||||||
|
value (ctx) {
|
||||||
|
const vals = this.args.map((a) => a.value(ctx))
|
||||||
|
return call(vals, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
get type () { return type }
|
||||||
|
}
|
||||||
|
registerFunctionClass(name, F)
|
||||||
|
return F
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenNumeric (vals) {
|
||||||
|
const out = [];
|
||||||
|
(function walk (v) {
|
||||||
|
if (Array.isArray(v)) v.forEach(walk)
|
||||||
|
else out.push(toNumber(v))
|
||||||
|
})(vals)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
defineFunction('abs', { min: 1, max: 1, call: (v) => Math.abs(toNumber(v[0])) })
|
||||||
|
defineFunction('min', { min: 1, call: (v) => Math.min.apply(null, flattenNumeric(v)) })
|
||||||
|
defineFunction('max', { min: 1, call: (v) => Math.max.apply(null, flattenNumeric(v)) })
|
||||||
|
defineFunction('sum', { min: 1, call: (v) => flattenNumeric(v).reduce((a, b) => a + b, 0) })
|
||||||
|
defineFunction('avg', {
|
||||||
|
min: 1,
|
||||||
|
call: (v) => {
|
||||||
|
const nums = flattenNumeric(v)
|
||||||
|
if (nums.length === 0) return 0
|
||||||
|
return nums.reduce((a, b) => a + b, 0) / nums.length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defineFunction('count', {
|
||||||
|
min: 1,
|
||||||
|
max: 1,
|
||||||
|
call: (v) => Array.isArray(v[0]) ? v[0].length : (v[0] == null ? 0 : 1)
|
||||||
|
})
|
||||||
|
defineFunction('round', {
|
||||||
|
min: 1,
|
||||||
|
max: 2,
|
||||||
|
call: (v) => {
|
||||||
|
const n = toNumber(v[0])
|
||||||
|
const p = v.length > 1 ? toNumber(v[1]) : 0
|
||||||
|
const f = Math.pow(10, p)
|
||||||
|
return Math.round(n * f) / f
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defineFunction('roundup', {
|
||||||
|
min: 1,
|
||||||
|
max: 2,
|
||||||
|
call: (v) => {
|
||||||
|
const n = toNumber(v[0])
|
||||||
|
const p = v.length > 1 ? toNumber(v[1]) : 0
|
||||||
|
const f = Math.pow(10, p)
|
||||||
|
return (n < 0 ? -Math.floor(-n * f) : Math.ceil(n * f)) / f
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defineFunction('rounddown', {
|
||||||
|
min: 1,
|
||||||
|
max: 2,
|
||||||
|
call: (v) => {
|
||||||
|
const n = toNumber(v[0])
|
||||||
|
const p = v.length > 1 ? toNumber(v[1]) : 0
|
||||||
|
const f = Math.pow(10, p)
|
||||||
|
return (n < 0 ? -Math.ceil(-n * f) : Math.floor(n * f)) / f
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const MATH_FNS = {
|
||||||
|
sqrt: Math.sqrt,
|
||||||
|
cbrt: Math.cbrt,
|
||||||
|
sin: Math.sin,
|
||||||
|
cos: Math.cos,
|
||||||
|
tan: Math.tan,
|
||||||
|
asin: Math.asin,
|
||||||
|
acos: Math.acos,
|
||||||
|
atan: Math.atan,
|
||||||
|
sinh: Math.sinh,
|
||||||
|
cosh: Math.cosh,
|
||||||
|
tanh: Math.tanh,
|
||||||
|
exp: Math.exp,
|
||||||
|
log: Math.log,
|
||||||
|
log2: Math.log2,
|
||||||
|
log10: Math.log10,
|
||||||
|
floor: Math.floor,
|
||||||
|
ceil: Math.ceil
|
||||||
|
}
|
||||||
|
Object.keys(MATH_FNS).forEach((name) => {
|
||||||
|
const fn = MATH_FNS[name]
|
||||||
|
defineFunction(name, { min: 1, max: 1, call: (v) => fn(toNumber(v[0])) })
|
||||||
|
})
|
||||||
|
defineFunction('atan2', { min: 2, max: 2, call: (v) => Math.atan2(toNumber(v[0]), toNumber(v[1])) })
|
||||||
|
defineFunction('hypot', { min: 1, call: (v) => Math.hypot.apply(null, flattenNumeric(v)) })
|
||||||
|
defineFunction('pow', { min: 2, max: 2, call: (v) => Math.pow(toNumber(v[0]), toNumber(v[1])) })
|
||||||
|
defineFunction('pi', { min: 0, max: 0, call: () => Math.PI })
|
||||||
|
defineFunction('e', { min: 0, max: 0, call: () => Math.E })
|
||||||
|
|
||||||
|
defineFunction('not', { min: 1, max: 1, type: 'logical', call: (v) => !v[0] })
|
||||||
|
defineFunction('and', { min: 1, type: 'logical', call: (v) => v.every((x) => Boolean(x)) })
|
||||||
|
defineFunction('or', { min: 1, type: 'logical', call: (v) => v.some((x) => Boolean(x)) })
|
||||||
|
defineFunction('xor', {
|
||||||
|
min: 2,
|
||||||
|
type: 'logical',
|
||||||
|
call: (v) => v.reduce((acc, x) => acc !== Boolean(x), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
class IfFunction extends FunctionNode {
|
||||||
|
static arity = 3
|
||||||
|
static minParamCount () { return 3 }
|
||||||
|
static maxParamCount () { return 3 }
|
||||||
|
value (ctx) {
|
||||||
|
return this.args[0].value(ctx) ? this.args[1].value(ctx) : this.args[2].value(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
get type () { return this.args[1].type }
|
||||||
|
dependencies (ctx) {
|
||||||
|
try {
|
||||||
|
return this.args[0].value(ctx) ? this.args[1].dependencies(ctx) : this.args[2].dependencies(ctx)
|
||||||
|
} catch (e) {
|
||||||
|
return uniq([].concat.apply([], this.args.map((a) => a.dependencies(ctx))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerFunctionClass('if', IfFunction)
|
||||||
|
|
||||||
|
defineFunction('length', { min: 1, max: 1, call: (v) => v[0] == null ? 0 : String(v[0]).length })
|
||||||
|
defineFunction('upcase', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).toUpperCase() })
|
||||||
|
defineFunction('downcase', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).toLowerCase() })
|
||||||
|
defineFunction('concat', { min: 1, type: 'string', call: (v) => v.map((x) => String(x)).join('') })
|
||||||
|
defineFunction('left', {
|
||||||
|
min: 2,
|
||||||
|
max: 2,
|
||||||
|
type: 'string',
|
||||||
|
call: (v) => String(v[0]).slice(0, toNumber(v[1]))
|
||||||
|
})
|
||||||
|
defineFunction('right', {
|
||||||
|
min: 2,
|
||||||
|
max: 2,
|
||||||
|
type: 'string',
|
||||||
|
call: (v) => {
|
||||||
|
const n = toNumber(v[1])
|
||||||
|
return n === 0 ? '' : String(v[0]).slice(-n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defineFunction('mid', {
|
||||||
|
min: 3,
|
||||||
|
max: 3,
|
||||||
|
type: 'string',
|
||||||
|
call: (v) => {
|
||||||
|
const s = String(v[0])
|
||||||
|
const start = toNumber(v[1]) - 1
|
||||||
|
const len = toNumber(v[2])
|
||||||
|
return s.slice(start, start + len)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defineFunction('trim', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).trim() })
|
||||||
|
defineFunction('contains', {
|
||||||
|
min: 2,
|
||||||
|
max: 2,
|
||||||
|
type: 'logical',
|
||||||
|
call: (v) => {
|
||||||
|
if (Array.isArray(v[0])) return v[0].indexOf(v[1]) !== -1
|
||||||
|
return String(v[0]).indexOf(String(v[1])) !== -1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const OP_CLASSES = {
|
||||||
|
add: Addition,
|
||||||
|
subtract: Subtraction,
|
||||||
|
multiply: Multiplication,
|
||||||
|
divide: Division,
|
||||||
|
pow: Exponentiation,
|
||||||
|
negate: Negation,
|
||||||
|
mod: Modulo,
|
||||||
|
bitor: BitwiseOr,
|
||||||
|
bitand: BitwiseAnd,
|
||||||
|
bitshiftleft: BitwiseShiftLeft,
|
||||||
|
bitshiftright: BitwiseShiftRight,
|
||||||
|
lt: LessThan,
|
||||||
|
gt: GreaterThan,
|
||||||
|
le: LessThanOrEqual,
|
||||||
|
ge: GreaterThanOrEqual,
|
||||||
|
ne: NotEqual,
|
||||||
|
eq: Equal,
|
||||||
|
and: And,
|
||||||
|
or: Or
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOperationClass (cls) {
|
||||||
|
return cls && (cls.prototype instanceof Operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse (tokens) {
|
||||||
|
if (!tokens || tokens.length === 0) return new NilNode()
|
||||||
|
|
||||||
|
const output = []
|
||||||
|
const operations = []
|
||||||
|
const arities = []
|
||||||
|
const skip = new Set()
|
||||||
|
|
||||||
|
function consume (count) {
|
||||||
|
if (count == null) count = 2
|
||||||
|
const op = operations.pop()
|
||||||
|
if (!op) throw new ParseError('invalid statement')
|
||||||
|
|
||||||
|
const outputSize = output.length
|
||||||
|
const opMin = (typeof op.minParamCount === 'function') ? op.minParamCount() : null
|
||||||
|
const opMax = (typeof op.maxParamCount === 'function') ? op.maxParamCount() : null
|
||||||
|
|
||||||
|
const actualArgs = (op.arity != null) ? op.arity : count
|
||||||
|
const minSize = (op.arity != null) ? op.arity : (opMin != null ? opMin : count)
|
||||||
|
const maxSize = (op.arity != null) ? op.arity : (opMax != null ? opMax : count)
|
||||||
|
|
||||||
|
if (outputSize < minSize || actualArgs < minSize) {
|
||||||
|
throw new ParseError((op.name || 'operator') + ' has too few operands (given ' + outputSize + ', expected ' + minSize + ')')
|
||||||
|
}
|
||||||
|
if ((outputSize > maxSize && operations.length === 0) || actualArgs > maxSize) {
|
||||||
|
throw new ParseError((op.name || 'operator') + ' has too many operands (given ' + outputSize + ', expected ' + maxSize + ')')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op === ArrayNode && output.length === 0) {
|
||||||
|
output.push(new ArrayNode())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputSize < actualArgs) throw new ParseError('invalid statement')
|
||||||
|
const args = []
|
||||||
|
for (let i = 0; i < actualArgs; i++) args.unshift(output.pop())
|
||||||
|
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
output.push(new op(...args))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOperator (token, lookahead) {
|
||||||
|
let cls = OP_CLASSES[token.value]
|
||||||
|
if (!cls) throw new ParseError('Unknown operator ' + token.value)
|
||||||
|
if (typeof cls.resolveClass === 'function') cls = cls.resolveClass(lookahead)
|
||||||
|
const prec = cls.precedence
|
||||||
|
if (cls.rightAssociative) {
|
||||||
|
while (operations.length > 0) {
|
||||||
|
const top = operations[operations.length - 1]
|
||||||
|
if (isOperationClass(top) && prec < top.precedence) consume()
|
||||||
|
else break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (operations.length > 0) {
|
||||||
|
const top = operations[operations.length - 1]
|
||||||
|
if (isOperationClass(top) && prec <= top.precedence) consume()
|
||||||
|
else break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
operations.push(cls)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFunction (token) {
|
||||||
|
const fn = getFunctionClass(token.value)
|
||||||
|
if (!fn) throw new ParseError('Undefined function ' + token.value)
|
||||||
|
arities.push(0)
|
||||||
|
operations.push(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArray (token) {
|
||||||
|
if (token.value === 'array_start') {
|
||||||
|
operations.push(ArrayNode)
|
||||||
|
arities.push(0)
|
||||||
|
} else {
|
||||||
|
while (operations.length > 0 && operations[operations.length - 1] !== ArrayNode) consume()
|
||||||
|
if (operations[operations.length - 1] !== ArrayNode) throw new ParseError('Unbalanced bracket')
|
||||||
|
consume(arities.pop() + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGrouping (token, lookahead, index) {
|
||||||
|
if (token.value === 'open') {
|
||||||
|
if (lookahead && lookahead.value === 'close') {
|
||||||
|
skip.add(index + 1)
|
||||||
|
arities.pop()
|
||||||
|
consume(0)
|
||||||
|
} else {
|
||||||
|
operations.push(Grouping)
|
||||||
|
}
|
||||||
|
} else if (token.value === 'close') {
|
||||||
|
while (operations.length > 0 && operations[operations.length - 1] !== Grouping) consume()
|
||||||
|
const lparen = operations.pop()
|
||||||
|
if (lparen !== Grouping) throw new ParseError('Unbalanced parenthesis')
|
||||||
|
const top = operations[operations.length - 1]
|
||||||
|
if (top && (top.prototype instanceof FunctionNode)) consume(arities.pop() + 1)
|
||||||
|
} else if (token.value === 'comma') {
|
||||||
|
if (arities.length === 0) throw new ParseError('invalid statement')
|
||||||
|
arities[arities.length - 1] += 1
|
||||||
|
while (operations.length > 0 &&
|
||||||
|
operations[operations.length - 1] !== Grouping &&
|
||||||
|
operations[operations.length - 1] !== ArrayNode) consume()
|
||||||
|
} else {
|
||||||
|
throw new ParseError('Unknown grouping token ' + token.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
if (skip.has(i)) continue
|
||||||
|
const token = tokens[i]
|
||||||
|
const lookahead = tokens[i + 1]
|
||||||
|
|
||||||
|
switch (token.category) {
|
||||||
|
case 'numeric': output.push(new Numeric(token)); break
|
||||||
|
case 'logical': output.push(new Logical(token)); break
|
||||||
|
case 'string': output.push(new StringNode(token)); break
|
||||||
|
case 'identifier': output.push(new Identifier(token)); break
|
||||||
|
case 'null': output.push(new NilNode()); break
|
||||||
|
case 'operator':
|
||||||
|
case 'comparator':
|
||||||
|
case 'combinator':
|
||||||
|
handleOperator(token, lookahead); break
|
||||||
|
case 'function': handleFunction(token); break
|
||||||
|
case 'array': handleArray(token); break
|
||||||
|
case 'grouping': handleGrouping(token, lookahead, i); break
|
||||||
|
default:
|
||||||
|
throw new ParseError('Not implemented for tokens of category ' + token.category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (operations.length > 0) consume()
|
||||||
|
|
||||||
|
if (output.length !== 1) throw new ParseError('Invalid statement')
|
||||||
|
return output[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
class Calculator {
|
||||||
|
constructor (options) {
|
||||||
|
options = options || {}
|
||||||
|
this.caseSensitive = options.caseSensitive === true
|
||||||
|
this.memory = Object.create(null)
|
||||||
|
this._astCache = Object.create(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
key (k) { return this.caseSensitive ? k : String(k).toLowerCase() }
|
||||||
|
|
||||||
|
normalize (data) {
|
||||||
|
const out = Object.create(null)
|
||||||
|
if (!data) return out
|
||||||
|
for (const k of Object.keys(data)) out[this.key(k)] = data[k]
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
store (data) { Object.assign(this.memory, this.normalize(data)); return this }
|
||||||
|
bind (data) { return this.store(data) }
|
||||||
|
clear () { this.memory = Object.create(null); return this }
|
||||||
|
|
||||||
|
ast (expression) {
|
||||||
|
const cached = this._astCache[expression]
|
||||||
|
if (cached) return cached
|
||||||
|
const node = parse(tokenize(expression, { caseSensitive: this.caseSensitive }))
|
||||||
|
this._astCache[expression] = node
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenize (expression) {
|
||||||
|
return tokenize(expression, { caseSensitive: this.caseSensitive })
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies (expression) {
|
||||||
|
return this.ast(expression).dependencies(this.memory)
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluate (expression, data, onError) {
|
||||||
|
try {
|
||||||
|
return this.evaluateStrict(expression, data)
|
||||||
|
} catch (e) {
|
||||||
|
if (typeof data === 'function') return data(expression, e)
|
||||||
|
if (typeof onError === 'function') return onError(expression, e)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluateStrict (expression, data) {
|
||||||
|
const ctx = Object.assign(Object.create(null), this.memory, this.normalize(data))
|
||||||
|
const node = this.ast(expression)
|
||||||
|
return node.value(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
addFunction (name, spec) {
|
||||||
|
defineFunction(name, {
|
||||||
|
min: spec.minArgs == null ? (spec.min == null ? 0 : spec.min) : spec.minArgs,
|
||||||
|
max: spec.maxArgs == null ? (spec.max == null ? Infinity : spec.max) : spec.maxArgs,
|
||||||
|
type: spec.type || 'numeric',
|
||||||
|
call: spec.call
|
||||||
|
})
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Calculator,
|
||||||
|
tokenize,
|
||||||
|
parse,
|
||||||
|
CalculatorError,
|
||||||
|
ParseError,
|
||||||
|
TokenizerError,
|
||||||
|
UnboundVariableError,
|
||||||
|
ZeroDivisionError,
|
||||||
|
ArgumentError
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addFunction (name, spec) { defineFunction(name, spec) }
|
||||||
@ -1,36 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Submitters
|
|
||||||
module MaybeAssignDefaultEmailSignature
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def call(submitter, params, attachments)
|
|
||||||
return if params[:t].present? && params[:t] != SubmissionEvents.build_tracking_param(submitter, 'click_email')
|
|
||||||
return if params[:t].blank? && !submitter.submission_events.exists?(event_type: :click_email)
|
|
||||||
|
|
||||||
signature_attachment = find_previous_signature(submitter)
|
|
||||||
|
|
||||||
return if signature_attachment.blank?
|
|
||||||
|
|
||||||
existing_attachment = attachments.find do |a|
|
|
||||||
a.blob_id == signature_attachment.blob_id && submitter.id == a.record_id
|
|
||||||
end
|
|
||||||
|
|
||||||
return existing_attachment if existing_attachment
|
|
||||||
|
|
||||||
submitter.attachments_attachments.create_or_find_by!(blob_id: signature_attachment.blob_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_previous_signature(submitter)
|
|
||||||
return if submitter.email.blank?
|
|
||||||
|
|
||||||
submitters_query =
|
|
||||||
Submitter.where(email: submitter.email)
|
|
||||||
.where.not(completed_at: nil)
|
|
||||||
.where(SubmissionEvent.where(Submitter.arel_table[:id].eq(SubmissionEvent.arel_table[:submitter_id]))
|
|
||||||
.where(event_type: :click_email).limit(1).arel.exists)
|
|
||||||
|
|
||||||
ActiveStorage::Attachment.where(name: :signature, record: submitters_query).order(:id).last
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Users
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def generate_csv(users)
|
||||||
|
headers = %w[email first_name last_name role current_sign_in_at last_sign_in_at updated_at created_at]
|
||||||
|
|
||||||
|
CSVSafe.generate do |csv|
|
||||||
|
csv << headers
|
||||||
|
|
||||||
|
users.each { |user| csv << user.values_at(*headers) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in new issue