diff --git a/Procfile b/Procfile index 18cad01..6d49bab 100644 --- a/Procfile +++ b/Procfile @@ -1,5 +1,5 @@ web: ./node_modules/.bin/http-server -c-1 . sass: ./node_modules/.bin/node-sass --recursive --output demo demo && ./node_modules/.bin/node-sass --watch --recursive --output demo demo webpack: webpack -w -pug: ./node_modules/.bin/pug -w index.pug demo +pug: ./node_modules/.bin/pug -w index.pug demo test tests: ./node_modules/.bin/karma start diff --git a/README.md b/README.md index f9a8aa5..b0b9616 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ the changes. (You will only get scroll issues with fixed elements) getComputedElement... - [ ] Add more tests and edge cases... - [ ] Manage scroll (globally and on elements) +- [ ] Round in the tests +- [ ] Greg's test +- [ ] CamelCase + check webkit naming convention ### Big Thanks diff --git a/demo/demo.coffee b/demo/demo.coffee index 477ff1f..12dd6e2 100644 --- a/demo/demo.coffee +++ b/demo/demo.coffee @@ -1,23 +1,45 @@ $ -> + parse_assert = (input)-> + $.map input.split(':'), (i)-> + [ + $.map i.split(','), (s)-> + parseFloat(s) + ] + add_marker = (point)-> + $marker = $('
') + $marker.css('top', point[1]-5) + $marker.css('left', point[0]-5) + $marker.attr('data-x', point[0]) + $marker.attr('data-y', point[1]) + $('body').append($marker) + $('.referentiel').each -> - console.log this ref = new Referentiel(this) - $(this).on 'click', (e)-> - input = [e.pageX, e.pageY] - p = ref.global_to_local(input) - console.log input, '->', p - $pointer = $('.pointer', this) - $pointer.css('left', p[0]-3) - $pointer.css('top', p[1]-3) + $('[data-assert]', this).each (assert)-> + [global, local] = parse_assert $(this).data('assert') + result = ref.global_to_local(global) + console.log this, global, result, local + add_marker(global) + $assert = $(this) + $assert.css('left', result[0]-3) + $assert.css('top', result[1]-3) + $assert.attr('cx', result[0]) + $assert.attr('cy', result[1]) + $assert.data('x', result[0]) + $assert.data('y', result[1]) - $('body').on 'mousemove', (e)-> - return + $('body').on 'click', (e)-> $('.referentiel').each -> - console.log this ref = new Referentiel(this) input = [e.pageX, e.pageY] p = ref.global_to_local(input) - console.log input, '->', p + if $('.pointer', this).length == 0 + $pointer = $('') + $(this).append($pointer) $pointer = $('.pointer', this) + console.log '======' + console.log input, '->', p, new Referentiel(this).matrix(), this $pointer.css('left', p[0]-3) $pointer.css('top', p[1]-3) + $pointer.attr('cx', p[0]) + $pointer.attr('cy', p[1]) diff --git a/demo/demo.sass b/demo/demo.sass index d19d86c..6773b87 100644 --- a/demo/demo.sass +++ b/demo/demo.sass @@ -5,8 +5,21 @@ position: absolute border-radius: 50% .referentiel - width: 200px - height: 200px + width: 100px + height: 100px background: pink overflow: hidden position: relative +.marker + width: 10px + height: 10px + background: rgba(255, 0, 0, 0.5) + position: fixed + border-radius: 50% +.assert + position: absolute + width: 6px + height: 6px + background: black + position: absolute + border-radius: 50% diff --git a/demo/index.pug b/demo/index.pug index a9b8958..4a81478 100644 --- a/demo/index.pug +++ b/demo/index.pug @@ -8,11 +8,12 @@ html(lang='en') script(type='text/javascript', src='../dist/referentiel.js') script(type='text/javascript', src='../node_modules/jquery/dist/jquery.js') script(type='text/javascript', src='http://coffeescript.org/v1/browser-compiler/coffee-script.js') - script(type='text/javascript', src='coffee-script.js') + //script(type='text/javascript', src='coffee-script.js') script(type='text/coffeescript', src='demo.coffee') link(rel="stylesheet" media="all" href="../node_modules/reset-css/reset.css") link(rel="stylesheet" media="all" href="demo.css") body + include ../test/svg-viewport-kevan.pug //.super(style="height: 200px; width: 200px;") //div(style="margin-top: 40px; margin-left: 40px") //div(style="margin-top: 40px") @@ -24,7 +25,13 @@ html(lang='en') .referentiel .pointer //div(style="margin-top: 14px") - div(style="position: absolute; top: 40px; left: 50px;") + div(style="position: relative; top: 40px; left: 40px") + p Hello World + div + div(style="position: absolute; top: 40px; left: 50px;") + .referentiel + .pointer + //div(style="position: absolute; top: 40px; left: 50px;") .referentiel .pointer //div diff --git a/karma.conf.coffee b/karma.conf.coffee index 597554d..bb91ad3 100644 --- a/karma.conf.coffee +++ b/karma.conf.coffee @@ -110,6 +110,7 @@ module.exports = (config) -> 'node_modules/reset-css/reset.css', 'node_modules/jquery/dist/jquery.js', { pattern: 'test/**/*.coffee', included: true } + { pattern: 'test/**/*.html', served: true, included: false} ] preprocessors: { @@ -127,14 +128,14 @@ module.exports = (config) -> # - config.LOG_DEBUG logLevel: config.LOG_INFO - autoWatch: false + autoWatch: true # browsers: [ 'Chrome', 'Firefox' ] browsers: Object.keys(customLaunchers) # Continuous Integration mode # if true, Karma captures browsers, runs the tests and exits - singleRun: true + singleRun: false port: 9876 colors: true diff --git a/package.json b/package.json index ff7ee29..8b3c77b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "src" ], "scripts": { - "test": "npm run build && ./node_modules/.bin/karma start", + "test": "npm run build && ./node_modules/.bin/karma start --single-run", + "test-watch": "npm run build && ./node_modules/.bin/karma start", "start": "npm run build && ./node_modules/.bin/nf start", "build": "./node_modules/.bin/webpack" }, @@ -33,12 +34,15 @@ "jasmine": "2.8.0", "jasmine-core": "2.8.0", "jquery": "3.2.1", - "karma": "1.7.0", + "jsdom": "^11.5.1", + "karma": "^1.7.0", "karma-browserstack-launcher": "1.3.0", "karma-chrome-launcher": "2.2.0", "karma-coffee-preprocessor": "1.0.1", "karma-firefox-launcher": "1.0.1", "karma-jasmine": "1.1.0", + "karma-jsdom-launcher": "^6.1.2", + "karma-phantomjs-launcher": "^1.0.4", "karma-sauce-launcher": "1.2.0", "mocha": "3.5.0", "node-sass": "4.5.3", diff --git a/src/matrix_utils.coffee b/src/matrix_utils.coffee new file mode 100644 index 0000000..cbabe9f --- /dev/null +++ b/src/matrix_utils.coffee @@ -0,0 +1,50 @@ +MatrixUtils = { + identity: -> + [[1,0,0], [0,1,0], [0,0,1]] + det: (m)-> + return ( + m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2]) - + m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]) + ) + inv: (m)-> + invdet = 1.0/MatrixUtils.det(m) + return [ + [ + (m[1][1] * m[2][2] - m[2][1] * m[1][2]) * invdet, + (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * invdet, + (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * invdet, + ], + [ + (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * invdet, + (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * invdet, + (m[1][0] * m[0][2] - m[0][0] * m[1][2]) * invdet, + ], + [ + (m[1][0] * m[2][1] - m[2][0] * m[1][1]) * invdet, + (m[2][0] * m[0][1] - m[0][0] * m[2][1]) * invdet, + (m[0][0] * m[1][1] - m[1][0] * m[0][1]) * invdet, + ] + ] + mult: -> + [a, b, others...] = arguments + res = [] + for i in [0...3] + res[i] = [] + for j in [0...3] + res[i][j] = 0.0 + for k in [0...3] + res[i][j] += a[i][k]*b[k][j] + if others.length > 0 + MatrixUtils.mult(res, others...) + else + res + multVector: (m, v)-> + res = [] + for i in [0...3] + res[i] = 0.0 + for k in [0...3] + res[i] += m[i][k]*v[k] + res +} +module.exports = MatrixUtils diff --git a/src/referentiel.coffee b/src/referentiel.coffee index 23a60f4..17a7829 100644 --- a/src/referentiel.coffee +++ b/src/referentiel.coffee @@ -1,13 +1,14 @@ +TransformParser = require('./transform_parser.coffee') +MatrixUtils = require('./matrix_utils.coffee') module.exports = class Referentiel - constructor: (@reference)-> + constructor: (@reference, @options = {})-> global_to_local: (point)-> - @_multiply_point(@matrix_inv(), point) + @_multiplyPoint(@matrixInv(), point) local_to_global: (point)-> - @_multiply_point(@matrix(), point) - - _multiply_point: (matrix, point)-> + @_multiplyPoint(@matrix(), point) + _multiplyPoint: (matrix, point)-> v = [point[0], point[1], 1] - res = @_multiply_vector(matrix, v) + res = MatrixUtils.multVector(matrix, v) [ @_export(res[0]), @_export(res[1]) ] _export: (value)-> res = @_round(value) @@ -16,153 +17,117 @@ module.exports = class Referentiel _round: (value)-> precision = 1000000.0 Math.round(precision*value)/precision - clear_cache: -> - delete @_matrix_inv - delete @_matrix - delete @_matrix_transformation - delete @_matrix_transform_origin - - matrix_inv: -> - return @_matrix_inv if @_matrix_inv - @_matrix_inv = @matrix_inv_compute() - @_matrix_inv - matrix_inv_compute: -> - @_inv(@matrix()) + matrixInv: -> + MatrixUtils.inv(@matrix()) matrix: -> - return @_matrix if @_matrix - @_matrix = @matrix_compute() - @_matrix - matrix_compute: -> - matrix_locale = @matrix_locale() - if @getPropertyValue('position') == 'fixed' - return matrix_locale - if @reference.parentElement? - parent_referentiel = new Referentiel(@reference.parentElement) - return @_multiply(parent_referentiel.matrix(), matrix_locale) - matrix_locale - - matrix_locale: -> - return @_matrix_locale if @_matrix_locale - @_matrix_locale = @matrix_locale_compute() - @_matrix_locale - matrix_locale_compute: -> - @_multiply( - @matrix_offset(), - @_multiply( - @matrix_transformation_with_origin(), - @matrix_border() - ), - ) - - matrix_transformation_with_origin: -> - return @_matrix_transformation_with_origin if @_matrix_transformation_with_origin - @_matrix_transformation_with_origin = @matrix_transformation_with_origin_compute() - @_matrix_transformation_with_origin - matrix_transformation_with_origin_compute: -> - @_multiply( - @_multiply( - @matrix_transform_origin(), - @matrix_transformation() - ), - @_inv(@matrix_transform_origin()) + matrixLocale = @matrixLocale() + if @css('position') == 'fixed' + return matrixLocale + parent = @parent() + if parent + return MatrixUtils.mult( + (new Referentiel( + parent, + offsetParent: @reference.offsetParent + )).matrix(), + matrixLocale + ) + matrixLocale + matrixLocale: -> + MatrixUtils.mult( + @matrixSVGViewbox(), + @matrixOffset(), + @matrixTransformOrigin(), + @matrixTransform(), + MatrixUtils.inv(@matrixTransformOrigin()), + @matrixBorder() ) - - matrix_transformation: -> - return @_matrix_transformation if @_matrix_transformation - @_matrix_transformation = @matrix_transformation_compute() - @_matrix_transformation - matrix_transformation_compute: -> - transform = @getPropertyValue('transform') + matrixTransform: -> + transform = @reference.getAttribute('transform') || 'none' + transform = @reference.style.transform unless transform.match(/^matrix\((.*)\)$/) + transform = @css('transform') unless transform.match(/^matrix\((.*)\)$/) if res = transform.match(/^matrix\((.*)\)$/) - floats = res[1].split(',').map((e)-> + floats = res[1].replace(',', ' ').replace(' ', ' ').split(' ').map((e)-> parseFloat(e) ) return [[floats[0], floats[2], floats[4]],[floats[1], floats[3], floats[5]], [0, 0, 1]] - [[1,0,0], [0,1,0], [0,0,1]] + MatrixUtils.identity() - matrix_transform_origin: -> - return @_matrix_transform_origin if @_matrix_transform_origin - @_matrix_transform_origin = @matrix_transform_origin_compute() - @_matrix_transform_origin - - matrix_transform_origin_compute: -> - transform_origin = @getPropertyValue('transform-origin').replace(/px/g, '').split(' ').map (v)-> + matrixTransformOrigin: -> + transform_origin = @css('transform-origin').replace(/px/g, '').split(' ').map (v)-> parseFloat(v) [[1,0, transform_origin[0]], [0, 1, transform_origin[1]],[0,0,1]] - matrix_border: -> - return @_matrix_border if @_matrix_border - @_matrix_border = @matrix_border_compute() - @_matrix_border - matrix_border_compute: -> - left = parseFloat(@getPropertyValue('border-left-width').replace(/px/g, '') || 0) - top = parseFloat(@getPropertyValue('border-top-width').replace(/px/g, '') || 0) + matrixBorder: -> + left = parseFloat(@css('border-left-width').replace(/px/g, '') || 0) + top = parseFloat(@css('border-top-width').replace(/px/g, '') || 0) [[1,0,left],[0,1,top],[0,0,1]] - matrix_offset: -> - return @_matrix_offset if @_matrix_offset - @_matrix_offset = @matrix_offset_compute() - @_matrix_offset - matrix_offset_compute: -> - left = @reference.offsetLeft - top = @reference.offsetTop - switch @getPropertyValue('position') + parent: (element)-> + element ||= @reference + if element.parentNode? && element.parentNode != document.documentElement + element.parentNode + else + null + matrixOffset: -> + [left, top] = @offset() + switch @css('position') when 'absolute' return [[1,0,left],[0,1,top],[0,0,1]] - when 'fixed' + when 'fixed' left += window.pageXOffset top += window.pageYOffset return [[1,0,left],[0,1,top],[0,0,1]] - if @reference.parentElement? - parent = @reference.parentElement - parent_position = @getPropertyValue('position',parent) - if parent_position == 'static' - left -= parent.offsetLeft - top -= parent.offsetTop - + if @options.offsetParent? + if @options.offsetParent != @reference + [left, top] = [0, 0] [[1,0,left],[0,1,top],[0,0,1]] - - getPropertyValue: (property, element = null)-> - return Referentiel.jquery(element || @reference).css(property) if Referentiel.jquery - window.getComputedStyle(element || @reference).getPropertyValue(property) - _multiply_vector: (m, v)-> - res = [] - for i in [0...3] - res[i] = 0.0 - for k in [0...3] - res[i] += m[i][k]*v[k] - res - _multiply: (a, b)-> - res = [] - for i in [0...3] - res[i] = [] - for j in [0...3] - res[i][j] = 0.0 - for k in [0...3] - res[i][j] += a[i][k]*b[k][j] - res - _det: (m)-> - return ( - m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2]) - - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + - m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]) - ) - _inv: (m)-> - invdet = 1.0/@_det(m) - return [ + matrixSVGViewbox: -> + return MatrixUtils.identity() unless @reference instanceof SVGElement + size = [ + parseFloat(@css('width').replace(/px/g, '')) + parseFloat(@css('height').replace(/px/g, '')) + ] + attr = @reference.getAttribute('viewBox') + return MatrixUtils.identity() unless attr? + viewBox = attr.replace(',', ' ').replace(' ', ' ').split(' ').map (e)-> + parseFloat(e) + scale = [size[0] / viewBox[2], size[1] / viewBox[3] ] + MatrixUtils.mult( [ - (m[1][1] * m[2][2] - m[2][1] * m[1][2]) * invdet, - (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * invdet, - (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * invdet, + [scale[0], 0, 0] + [0, scale[1], 0] + [0, 0, 1] ], [ - (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * invdet, - (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * invdet, - (m[1][0] * m[0][2] - m[0][0] * m[1][2]) * invdet, + [1, 0, -viewBox[0]], + [0, 1, -viewBox[1]], + [0, 0, 1] ], - [ - (m[1][0] * m[2][1] - m[2][0] * m[1][1]) * invdet, - (m[2][0] * m[0][1] - m[0][0] * m[2][1]) * invdet, - (m[0][0] * m[1][1] - m[1][0] * m[0][1]) * invdet, - ] - ] + ) + offset: (element = null)-> + element ||= @reference + return [element.offsetLeft, element.offsetTop] if element.offsetLeft? + pos = @reference.getBoundingClientRect() + offset = [pos.left, pos.top] + parent = @parent(element) + if parent? + ppos = parent.getBoundingClientRect() + offset[0] -= ppos.left + offset[1] -= ppos.top + offset + css: (property, element = null)-> + element ||= @reference + return Referentiel.jquery(element).css(property) if Referentiel.jquery + window.getComputedStyle(element).getPropertyValue(property) +cache = (klass, functionName)-> + func = klass.prototype[functionName] + klass.prototype[functionName] = -> + @_cache ||= {} + unless @_cache[functionName] + @_cache[functionName] = func.apply(this, arguments) + @_cache[functionName] +cache Referentiel, 'matrix' +cache Referentiel, 'matrixInv' +Referentiel.MatrixUtils = MatrixUtils +Referentiel.TransformParser = TransformParser diff --git a/src/transform_parser.coffee b/src/transform_parser.coffee new file mode 100644 index 0000000..714306f --- /dev/null +++ b/src/transform_parser.coffee @@ -0,0 +1,78 @@ +MatrixUtils = require('./matrix_utils.coffee') +module.exports = TransformParser = { + parse: (input)-> + return 'none' if input == 'none' || input == '' + @to_css( + @export_matrix( + @parse_matrix(input) + ) + ) + export_matrix: (m)-> + res = [] + for i in [0...3] + res[i] = [] + for j in [0...3] + res[i][j] = @export_value(m[i][j]) + export_value: (value)-> + precison = 100000 + Math.round(value*precison)/precison + parse_matrix: (input)-> + matrix = [[1,0,0], [0,1,0], [0,0,1]] + inputs = input.split(')') + for i in inputs + matrix = MatrixUtils.mult( + matrix, + @parseOperation("#{i})") + ) + matrix + parseOperation: (input)-> + if match = input.match(/^[ ]*(rotate|translate|translateX|translateY|scale|scaleX|scaleY)\((.*)\)$/) + return @[match[1]](match[2]) + [[1,0,0], [0,1,0], [0,0,1]] + scale: (input)-> + scale = input.split(',') + scale[1] = scale[0] if scale.length == 1 + scale = [ + parseFloat(scale[0]) + parseFloat(scale[1]) + ] + [ + [scale[0],0,0] + [0,scale[1],0] + [0,0,1] + ] + scaleX: (input)-> + @scale("#{input},1") + scaleY: (input)-> + @scale("1, #{input}") + translateX: (input)-> + @translate(input) + translateY: (input)-> + @translate("0px, #{input}") + translate: (input)-> + left = 0 + right = 0 + if match = input.match(/^(.*)px$/) + left = parseFloat(match[1]) + if match = input.match(/^(.*)px, (.*)px$/) + left = parseFloat(match[1]) + right = parseFloat(match[2]) + [ + [1,0,left] + [0,1,right] + [0,0,1] + ] + rotate: (input)-> + angle = 0 + if match = input.match(/^(.*)deg$/) + angle = parseFloat(match[1]) * Math.PI / 180 + if match = input.match(/^(.*)turn$/) + angle = parseFloat(match[1]) * Math.PI * 2 + [ + [Math.cos(angle),-Math.sin(angle), 0], + [Math.sin(angle),Math.cos(angle), 0], + [0,0,1] + ] + to_css: (m)-> + "matrix(#{[m[0][0], m[1][0], m[0][1], m[1][1], m[0][2], m[1][2]].join(', ')})" +} diff --git a/test/borders.coffee b/test/borders.coffee deleted file mode 100644 index 82aa7bd..0000000 --- a/test/borders.coffee +++ /dev/null @@ -1,14 +0,0 @@ -describe 'borders', -> - it 'handle borders', -> - ref = from_template( - '' - ) - expect(ref.global_to_local([20, 10])).toEqual([10, 5]) - expect(ref.global_to_local([10, 5])).toEqual([0, 0]) - - it 'handle margin', -> - ref = from_template( - '' - ) - expect(ref.global_to_local([0, 0])).toEqual([-8, -6]) - expect(ref.global_to_local([8, 6])).toEqual([0, 0]) diff --git a/test/borders.pug b/test/borders.pug new file mode 100644 index 0000000..c357de4 --- /dev/null +++ b/test/borders.pug @@ -0,0 +1,3 @@ +.referentiel(style="border-top: 5px blue solid; border-left: 10px black solid;") + .assert(data-assert="10,5:0,0") + .assert(data-assert="110,105:100,100") diff --git a/test/helper.coffee b/test/helper.coffee index 0725683..ee0bc31 100644 --- a/test/helper.coffee +++ b/test/helper.coffee @@ -9,7 +9,3 @@ from_template = (template)-> $('.reference', $context)[0], $('.context', $context)[0] ) - -# afterEach -> -# console.log 'CLEAR' -# $('body').html('') diff --git a/test/margins.pug b/test/margins.pug new file mode 100644 index 0000000..59c8aa2 --- /dev/null +++ b/test/margins.pug @@ -0,0 +1,3 @@ +.referentiel(style="margin-top: 5px; margin-left: 10px;") + .assert(data-assert="10,5:0,0") + .assert(data-assert="110,105:100,100") diff --git a/test/position-basique.pug b/test/position-basique.pug new file mode 100644 index 0000000..9b541eb --- /dev/null +++ b/test/position-basique.pug @@ -0,0 +1,3 @@ +.referentiel(style="position: fixed; top: 50px; left: 20px;") + .assert(data-assert="20,50:0,0") + .assert(data-assert="30,60:10,10") diff --git a/test/position-in-flow.pug b/test/position-in-flow.pug new file mode 100644 index 0000000..3a91f4b --- /dev/null +++ b/test/position-in-flow.pug @@ -0,0 +1,5 @@ +div + div(style="width: 100%; height: 50px") + .referentiel + .assert(data-assert="0,50:0,0") + .assert(data-assert="50,50:50,0") diff --git a/test/position-offset.pug b/test/position-offset.pug new file mode 100644 index 0000000..07705d0 --- /dev/null +++ b/test/position-offset.pug @@ -0,0 +1,8 @@ +div(style="position: relative; top: 40px; left: 40px") + p Hello World + div + div(style="position: absolute; top: 40px; left: 50px;") + .referentiel + .assert(data-assert="90,80:0,0") + .assert(data-assert="140,130:50,50") + .assert(data-assert="190,180:100,100") diff --git a/test/position-scoped.pug b/test/position-scoped.pug new file mode 100644 index 0000000..8e8ce76 --- /dev/null +++ b/test/position-scoped.pug @@ -0,0 +1,6 @@ +div(style="position: fixed; top: 12px; left: 13px;") + div(style="position: fixed; top: 50px; left: 20px;") + div(style="position: fixed; top: 50px; left: 20px;") + .referentiel + .assert(data-assert="20,50:0,0") + .assert(data-assert="30,60:10,10") diff --git a/test/position.coffee b/test/position.coffee index 7e7ac8a..422a443 100644 --- a/test/position.coffee +++ b/test/position.coffee @@ -1,12 +1,5 @@ describe "Positions", -> describe 'Fixed', -> - it 'basic', -> - ref = from_template(' - - ') - expect(ref.global_to_local([20, 50])).toEqual([0, 0]) - expect(ref.global_to_local([30, 60])).toEqual([10, 10]) - it 'scoped', -> ref = from_template('