2019-05-02 18:17:27 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2019-01-17 06:46:11 -05:00
|
|
|
class ThemeJavascriptCompiler
|
|
|
|
|
|
|
|
module PrecompilerExtension
|
|
|
|
def initialize(theme_id)
|
|
|
|
super()
|
|
|
|
@theme_id = theme_id
|
|
|
|
end
|
|
|
|
|
|
|
|
def discourse_node_manipulator
|
|
|
|
<<~JS
|
|
|
|
|
|
|
|
// Helper to replace old themeSetting syntax
|
|
|
|
function generateHelper(settingParts) {
|
|
|
|
const settingName = settingParts.join('.');
|
|
|
|
return {
|
|
|
|
"path": {
|
|
|
|
"type": "PathExpression",
|
|
|
|
"original": "theme-setting",
|
|
|
|
"this": false,
|
|
|
|
"data": false,
|
|
|
|
"parts": [
|
|
|
|
"theme-setting"
|
|
|
|
],
|
|
|
|
"depth":0
|
|
|
|
},
|
|
|
|
"params": [
|
|
|
|
{
|
|
|
|
type: "NumberLiteral",
|
|
|
|
value: #{@theme_id},
|
|
|
|
original: #{@theme_id}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"type": "StringLiteral",
|
|
|
|
"value": settingName,
|
|
|
|
"original": settingName
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"hash": {
|
|
|
|
"type": "Hash",
|
|
|
|
"pairs": [
|
|
|
|
{
|
|
|
|
"type": "HashPair",
|
|
|
|
"key": "deprecated",
|
|
|
|
"value": {
|
|
|
|
"type": "BooleanLiteral",
|
|
|
|
"value": true,
|
|
|
|
"original": true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function manipulatePath(path) {
|
|
|
|
// Override old themeSetting syntax when it's a param inside another node
|
2019-02-06 16:09:21 -05:00
|
|
|
if(path.parts && path.parts[0] == "themeSettings"){
|
2019-01-17 06:46:11 -05:00
|
|
|
const settingParts = path.parts.slice(1);
|
|
|
|
path.type = "SubExpression";
|
|
|
|
Object.assign(path, generateHelper(settingParts))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function manipulateNode(node) {
|
2019-07-16 06:45:15 -04:00
|
|
|
// Magically add theme id as the first param for each of these helpers)
|
2019-02-06 16:09:21 -05:00
|
|
|
if (node.path.parts && ["theme-i18n", "theme-prefix", "theme-setting"].includes(node.path.parts[0])) {
|
2019-02-08 07:54:00 -05:00
|
|
|
if(node.params.length === 1){
|
|
|
|
node.params.unshift({
|
|
|
|
type: "NumberLiteral",
|
|
|
|
value: #{@theme_id},
|
|
|
|
original: #{@theme_id}
|
|
|
|
})
|
|
|
|
}
|
2019-01-17 06:46:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Override old themeSetting syntax when it's in its own node
|
2019-02-06 16:09:21 -05:00
|
|
|
if (node.path.parts && node.path.parts[0] == "themeSettings") {
|
2019-01-17 06:46:11 -05:00
|
|
|
Object.assign(node, generateHelper(node.path.parts.slice(1)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
JS
|
|
|
|
end
|
|
|
|
|
|
|
|
def source
|
|
|
|
[super, discourse_node_manipulator, discourse_extension].join("\n")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class RawTemplatePrecompiler < Barber::Precompiler
|
|
|
|
include PrecompilerExtension
|
|
|
|
|
|
|
|
def discourse_extension
|
|
|
|
<<~JS
|
|
|
|
let _superCompile = Handlebars.Compiler.prototype.compile;
|
|
|
|
Handlebars.Compiler.prototype.compile = function(program, options) {
|
|
|
|
|
|
|
|
// `replaceGet()` in raw-handlebars.js.es6 adds a `get` in front of things
|
2019-01-18 10:39:28 -05:00
|
|
|
// so undo this specific case for the old themeSettings.blah syntax
|
2019-01-17 06:46:11 -05:00
|
|
|
let visitor = new Handlebars.Visitor();
|
|
|
|
visitor.mutating = true;
|
|
|
|
visitor.MustacheStatement = (node) => {
|
|
|
|
if(node.path.original == 'get'
|
|
|
|
&& node.params
|
|
|
|
&& node.params[0]
|
2019-02-06 16:09:21 -05:00
|
|
|
&& node.params[0].parts
|
2019-01-18 10:39:28 -05:00
|
|
|
&& node.params[0].parts[0] == 'themeSettings'){
|
2019-01-17 06:46:11 -05:00
|
|
|
node.path.parts = node.params[0].parts
|
|
|
|
node.params = []
|
|
|
|
}
|
|
|
|
};
|
|
|
|
visitor.accept(program);
|
|
|
|
|
|
|
|
[
|
|
|
|
["SubExpression", manipulateNode],
|
|
|
|
["MustacheStatement", manipulateNode],
|
|
|
|
["PathExpression", manipulatePath]
|
|
|
|
].forEach((pass) => {
|
|
|
|
let visitor = new Handlebars.Visitor();
|
|
|
|
visitor.mutating = true;
|
|
|
|
visitor[pass[0]] = pass[1];
|
|
|
|
visitor.accept(program);
|
|
|
|
})
|
|
|
|
|
|
|
|
return _superCompile.apply(this, arguments);
|
|
|
|
};
|
|
|
|
JS
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class EmberTemplatePrecompiler < Barber::Ember::Precompiler
|
|
|
|
include PrecompilerExtension
|
|
|
|
|
|
|
|
def discourse_extension
|
|
|
|
<<~JS
|
2019-07-16 06:45:15 -04:00
|
|
|
Ember.HTMLBars.registerPlugin('ast', function() {
|
|
|
|
return {
|
|
|
|
name: 'theme-template-manipulator',
|
|
|
|
visitor: {
|
|
|
|
SubExpression: manipulateNode,
|
|
|
|
MustacheStatement: manipulateNode,
|
|
|
|
PathExpression: manipulatePath
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2019-01-17 06:46:11 -05:00
|
|
|
JS
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class CompileError < StandardError
|
|
|
|
end
|
|
|
|
|
|
|
|
attr_accessor :content
|
|
|
|
|
2019-05-24 10:25:55 -04:00
|
|
|
def initialize(theme_id, theme_name)
|
2019-01-17 06:46:11 -05:00
|
|
|
@theme_id = theme_id
|
2019-05-02 18:17:27 -04:00
|
|
|
@content = +""
|
2019-05-24 10:25:55 -04:00
|
|
|
@theme_name = theme_name
|
2019-01-17 06:46:11 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def prepend_settings(settings_hash)
|
|
|
|
@content.prepend <<~JS
|
|
|
|
(function() {
|
|
|
|
if ('Discourse' in window && Discourse.__container__) {
|
|
|
|
Discourse.__container__
|
|
|
|
.lookup("service:theme-settings")
|
|
|
|
.registerSettings(#{@theme_id}, #{settings_hash.to_json});
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
JS
|
|
|
|
end
|
|
|
|
|
|
|
|
# TODO Error handling for handlebars templates
|
|
|
|
def append_ember_template(name, hbs_template)
|
|
|
|
name = name.inspect
|
|
|
|
compiled = EmberTemplatePrecompiler.new(@theme_id).compile(hbs_template)
|
|
|
|
content << <<~JS
|
|
|
|
(function() {
|
|
|
|
if ('Ember' in window) {
|
|
|
|
Ember.TEMPLATES[#{name}] = Ember.HTMLBars.template(#{compiled});
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
JS
|
|
|
|
rescue Barber::PrecompilerError => e
|
|
|
|
raise CompileError.new e.instance_variable_get(:@error) # e.message contains the entire template, which could be very long
|
|
|
|
end
|
|
|
|
|
2020-03-06 11:35:52 -05:00
|
|
|
def raw_template_name(name)
|
|
|
|
name = name.sub(/\.(raw|hbr)$/, '')
|
|
|
|
name.inspect
|
|
|
|
end
|
|
|
|
|
2019-01-17 06:46:11 -05:00
|
|
|
def append_raw_template(name, hbs_template)
|
|
|
|
compiled = RawTemplatePrecompiler.new(@theme_id).compile(hbs_template)
|
|
|
|
@content << <<~JS
|
|
|
|
(function() {
|
|
|
|
if ('Discourse' in window) {
|
2020-03-06 11:35:52 -05:00
|
|
|
Discourse.RAW_TEMPLATES[#{raw_template_name(name)}] = requirejs('discourse-common/lib/raw-handlebars').template(#{compiled});
|
2019-01-17 06:46:11 -05:00
|
|
|
}
|
|
|
|
})();
|
|
|
|
JS
|
|
|
|
rescue Barber::PrecompilerError => e
|
|
|
|
raise CompileError.new e.instance_variable_get(:@error) # e.message contains the entire template, which could be very long
|
|
|
|
end
|
|
|
|
|
|
|
|
def append_plugin_script(script, api_version)
|
|
|
|
@content << transpile(script, api_version)
|
|
|
|
end
|
|
|
|
|
|
|
|
def append_raw_script(script)
|
|
|
|
@content << script + "\n"
|
|
|
|
end
|
|
|
|
|
2019-11-05 06:54:12 -05:00
|
|
|
def append_module(script, name, include_variables: true)
|
|
|
|
script = "#{theme_variables}#{script}" if include_variables
|
2020-03-11 09:43:55 -04:00
|
|
|
transpiler = DiscourseJsProcessor::Transpiler.new
|
|
|
|
@content << transpiler.perform(script, "", name)
|
2019-06-03 05:41:00 -04:00
|
|
|
rescue MiniRacer::RuntimeError => ex
|
|
|
|
raise CompileError.new ex.message
|
|
|
|
end
|
|
|
|
|
2019-01-17 06:46:11 -05:00
|
|
|
def append_js_error(message)
|
|
|
|
@content << "console.error('Theme Transpilation Error:', #{message.inspect});"
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2019-06-03 05:41:00 -04:00
|
|
|
def theme_variables
|
|
|
|
<<~JS
|
|
|
|
const __theme_name__ = "#{@theme_name.gsub('"', "\\\"")}";
|
|
|
|
const settings = Discourse.__container__
|
|
|
|
.lookup("service:theme-settings")
|
|
|
|
.getObjectForTheme(#{@theme_id});
|
|
|
|
const themePrefix = (key) => `theme_translations.#{@theme_id}.${key}`;
|
|
|
|
JS
|
|
|
|
end
|
|
|
|
|
2019-01-17 06:46:11 -05:00
|
|
|
def transpile(es6_source, version)
|
2020-03-11 09:43:55 -04:00
|
|
|
transpiler = DiscourseJsProcessor::Transpiler.new(skip_module: true)
|
2019-01-17 06:46:11 -05:00
|
|
|
wrapped = <<~PLUGIN_API_JS
|
2019-01-17 09:39:39 -05:00
|
|
|
(function() {
|
|
|
|
if ('Discourse' in window && typeof Discourse._registerPluginCode === 'function') {
|
2019-06-03 05:41:00 -04:00
|
|
|
#{theme_variables}
|
2019-01-17 09:39:39 -05:00
|
|
|
Discourse._registerPluginCode('#{version}', api => {
|
2019-05-24 10:25:55 -04:00
|
|
|
try {
|
2019-01-17 09:39:39 -05:00
|
|
|
#{es6_source}
|
2019-05-24 10:25:55 -04:00
|
|
|
} catch(err) {
|
|
|
|
const rescue = require("discourse/lib/utilities").rescueThemeError;
|
|
|
|
rescue(__theme_name__, err, api);
|
|
|
|
}
|
2019-01-17 09:39:39 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
})();
|
2019-01-17 06:46:11 -05:00
|
|
|
PLUGIN_API_JS
|
|
|
|
|
2020-03-11 09:43:55 -04:00
|
|
|
transpiler.perform(wrapped)
|
2019-01-17 06:46:11 -05:00
|
|
|
rescue MiniRacer::RuntimeError => ex
|
|
|
|
raise CompileError.new ex.message
|
|
|
|
end
|
|
|
|
end
|