diff --git a/app/controllers/static_site/entries_controller.rb b/app/controllers/static_site/entries_controller.rb index 5570d0d69..680745098 100644 --- a/app/controllers/static_site/entries_controller.rb +++ b/app/controllers/static_site/entries_controller.rb @@ -10,17 +10,13 @@ def show serve_blob(blob) elsif @entry.manifest? - render plain: @entry.body + render plain: @entry.render_stylesheet!, content_type: 'text/css' - # TODO: make up my mind on how to handle templates. - # elsif @entry.template? - # don't love it but fix later, lol do not deploy this to untrusted user contexts??? - # render inline: File.read(File.join(current_notebook.import_path, @entry.source)), layout: "application" - - elsif @entry.note? || @entry.bookmark? + elsif @entry.note? || @entry.bookmark? || @entry.template? @show_thread = params[:thread].present? @renderer = EntryRenderer.new(@entry, remove_subject: true) @current_date = @entry.occurred_at.strftime("%Y-%m-%d") + else render plain: "", status: 404 end @@ -38,6 +34,14 @@ def self.controller_path private def set_entry + if ENV["FFLAG_RELOAD"] + @entry = EntryImporter.new(current_notebook).resolve_and_import!(params[:id]) + + if @entry + return + end + end + # quick terrible hack for routing document type entries if params[:format] identifier = "#{params[:id]}.#{params[:format]}" diff --git a/app/models/ad_hoc_markdown_importer.rb b/app/models/ad_hoc_markdown_importer.rb index 58cdcc1a1..d68a19283 100644 --- a/app/models/ad_hoc_markdown_importer.rb +++ b/app/models/ad_hoc_markdown_importer.rb @@ -54,20 +54,19 @@ def process_import_path(notebook) identifier = path_to_relative_identifier(file_path, notebook.import_path) + entry_importer = EntryImporter.new(notebook) if skip_file_path?(identifier, file_path) # Do nothing. elsif looks_like_text?(file_path) # Files that become normal entries, the content in our site. puts "processing #{file_path}" if Rails.env.development? - entry_attributes = entry_attributes_from_markdown(identifier, file_path) - - entry = add_entry!(notebook, entry_attributes) + entry_importer.import!(identifier, file_path) else # everything that is not markdown is treated a bit differently. puts "processing #{file_path}" if Rails.env.development? entry_attributes = entry_attributes_from_document(identifier, file_path) - entry = add_entry!(notebook, entry_attributes) + entry = entry_importer.add_entry!(entry_attributes) filename = File.basename(identifier) if !entry.files.blobs.find_by(filename: filename) @@ -91,72 +90,7 @@ def skip_file_path?(identifier, file_path) end def looks_like_text?(file_path) - file_path =~ /\.(md|markdown|html)$/ - end - - def entry_attributes_from_markdown(identifier, md_path) - loader = FrontMatterParser::Loader::Yaml.new(allowlist_classes: [Time, Date, DateTime]) - md_parser = FrontMatterParser::SyntaxParser::Md.new - parsed_file = FrontMatterParser::Parser.new(md_parser, loader: loader).call(File.read(md_path)) - - occurred_at = parsed_file["occurred_at"] - if occurred_at.blank? - # let's try to guess it from the file - basename = File.basename(md_path) - date = basename.match(/([0-9]{4}-*[0-9]{2}-*[0-9]{2}-*)/).to_a[0] - - # most of the time this does the right thing but it does have the habit - # of sometimes throwing an exception - begin - occurred_at = DateTime.parse(date.to_s) - rescue ArgumentError - end - - # if still nil, let's look at the file itself - if occurred_at.nil? - occurred_at = File.ctime(md_path) - end - end - - created_at = parsed_file["created_at"] || File.ctime(md_path) - updated_at = parsed_file["updated_at"] || File.mtime(md_path) - - # if we're parsing front-mattered markdown, you don't get to define an - # identifier separate from the file's relative path, don't want to deal - # with collisions etc, too confusing, the ad hoc markdown is for ad hoc - # files, loosely slapped together! - # - # then, we lop off `.md`, `.markdown` and `html` from the suffix - entry_source = identifier # store the unmodified identifier as the "source" - identifier = identifier.gsub(/\.(md|markdown|html)/, "") - - entry_attributes = parsed_file.front_matter.merge({ - "identifier" => identifier, - "source" => entry_source, - "occurred_at" => occurred_at, - "body" => parsed_file.content, - "created_at" => created_at, - "updated_at" => updated_at, - skip_local_sync: true - }).slice(*Entry.accepted_attributes) - - # handle metadata! - metadata_keys = (parsed_file.front_matter.keys - Entry.accepted_attributes) - if metadata_keys.any? - # if the user has specified a non Hash value, ie "metadata: foo", then - # throw a slightly easier to understand error here, instead of later on - # when we try to instantiate the Entry object & the error gets thrown there. - if entry_attributes["metadata"] && !entry_attributes["metadata"].is_a?(Hash) - raise "I expected the 'metadata' key on #{identifier} to be Hash, but something else is going on." - end - - entry_attributes["metadata"] ||= {} - metadata_keys.each do |mkey| - entry_attributes["metadata"][mkey] ||= parsed_file.front_matter[mkey] - end - end - - entry_attributes + file_path =~ /\.(md|markdown|html|erb)$/ end def entry_attributes_from_document(identifier, file_path) @@ -169,7 +103,6 @@ def entry_attributes_from_document(identifier, file_path) if identifier == "stylesheets/application.css.scss" # TODO: should this also be a "template"? doesn't super matter. entry_kind = :manifest - entry_body = File.read(file_path) elsif identifier =~ /\.erb$/ # idea is that templates are rendered from within context of a # controller, which is too painful to setup here @@ -206,29 +139,15 @@ def process_templates(notebook) # if there is a stylesheets/application.css.scss we want to render the # Sass and convert it to a stylesheets/application.css if stylesheet = notebook.entries.manifests.find_by(identifier: "stylesheets/application.css.scss") - load_path = File.join(notebook.import_path, "stylesheets") - - rendered_css = SassC::Engine.new(stylesheet.body, { - filename: "application.css.scss", - syntax: :scss, - load_paths: [load_path], - }).render - # there can only be ONE application.css if to_delete = notebook.entries.find_by(identifier: "stylesheets/application.css") puts "Destroying extraneous stylesheets/application.css, so it can be replaced." to_delete.destroy end - rendered_stylesheet = notebook.entries.new(stylesheet.export_attributes) - rendered_stylesheet.identifier = "stylesheets/application.css" - rendered_stylesheet.kind = :document - rendered_stylesheet.save! - - blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(rendered_css), - filename: "application.css") - blob.analyze - rendered_stylesheet.files.create(blob_id: blob.id, created_at: blob.created_at) + stylesheet.identifier = "stylesheets/application.css" + stylesheet.render_stylesheet! + stylesheet.save end end @@ -236,19 +155,4 @@ def process_templates(notebook) def path_to_relative_identifier(file_path, import_path) Pathname.new(file_path).relative_path_from(import_path).to_s end - - def add_entry!(notebook, entry_attributes) - identifier = entry_attributes["identifier"] - - # find or update the entry - entry = notebook.entries.find_by(identifier: identifier) - - if entry - entry.update!(entry_attributes) - else - entry = notebook.entries.create(entry_attributes) - end - - entry - end end diff --git a/app/models/entry.rb b/app/models/entry.rb index 64cf01796..875cb7d68 100644 --- a/app/models/entry.rb +++ b/app/models/entry.rb @@ -442,4 +442,28 @@ def self.accepted_attributes "state", "hide"] end + + # ---- hack + SCSS_MANIFEST = "application.css.scss" + def render_stylesheet! + if self.manifest? + load_path = File.join(parent_notebook.import_path, "stylesheets") + manifest_path = File.join(load_path, SCSS_MANIFEST) + + if File.exist?(manifest_path) + if body.nil? || File.mtime(manifest_path) > updated_at + rendered_css = SassC::Engine.new(File.read(manifest_path), { + filename: SCSS_MANIFEST, + syntax: :scss, + load_paths: [load_path], + }).render + + self.body = rendered_css + self.save! + end + end + end + + self.body + end end diff --git a/app/models/entry_importer.rb b/app/models/entry_importer.rb new file mode 100644 index 000000000..5560edff8 --- /dev/null +++ b/app/models/entry_importer.rb @@ -0,0 +1,169 @@ +class EntryImporter + attr_reader :current_notebook, :root_path + def initialize(current_notebook) + @current_notebook = current_notebook + @root_path = current_notebook.import_path + end + + def resolve_and_import!(identifier) + + # case identifier + # when "stylesheets/application" + # return render_stylesheet + # end + # TODO: replace hacky & brittle with more well-defined search + # i.e. exact lookups vs just fuzzy searching + # ALSO: need to think about how this would interact with scss templates. + file_path = build_file_path(identifier) + glob_path = file_path + "*" + file_path = Dir[glob_path].select { |f| f =~ /#{file_path}(\.html|\.md|\.markdown)+/ }.reject {|f| File.directory?(f) }.first + if file_path && looks_like_text?(file_path) + # TODO: hrm, should the identifier come from the request? this will blow up somehow + import!(identifier, file_path) + else + puts "couldn't find nothin' to import" + return nil + end + end + + def import!(identifier, file_path) + entry_attributes = entry_attributes_from_markdown(identifier, file_path) + + entry = add_entry!(entry_attributes) + end + + def looks_like_text?(file_path) + file_path =~ /\.(md|markdown|html|erb)$/ + end + + def build_file_path(path_info) + clean_path_info = Rack::Utils.clean_path_info(path_info) + ::File.join(@root_path, clean_path_info) + end + + def entry_attributes_from_markdown(identifier, md_path) + loader = FrontMatterParser::Loader::Yaml.new(allowlist_classes: [Time, Date, DateTime]) + md_parser = FrontMatterParser::SyntaxParser::Md.new + parsed_file = FrontMatterParser::Parser.new(md_parser, loader: loader).call(File.read(md_path)) + + occurred_at = parsed_file["occurred_at"] + if occurred_at.blank? + # let's try to guess it from the file + basename = File.basename(md_path) + date = basename.match(/([0-9]{4}-*[0-9]{2}-*[0-9]{2}-*)/).to_a[0] + + # most of the time this does the right thing but it does have the habit + # of sometimes throwing an exception + begin + occurred_at = DateTime.parse(date.to_s) + rescue ArgumentError + end + + # if still nil, let's look at the file itself + if occurred_at.nil? + # TODO: should throw an error maybe? Git does not set ctime. + occurred_at = File.ctime(md_path) + end + end + + created_at = parsed_file["created_at"] || occurred_at + updated_at = parsed_file["updated_at"] || occurred_at + + # if we're parsing front-mattered markdown, you don't get to define an + # identifier separate from the file's relative path, don't want to deal + # with collisions etc, too confusing, the ad hoc markdown is for ad hoc + # files, loosely slapped together! + # + # then, we lop off `.md`, `.markdown` and `html` from the suffix + entry_source = md_path # store the unmodified identifier as the "source" + identifier = identifier.gsub(/\.(md|markdown|html|erb)/, "") + + # TODO: test this behaviour + if entry_source =~ /\.erb$/ + entry_kind = "template" + else + entry_kind = nil + end + + entry_attributes = parsed_file.front_matter.merge({ + "identifier" => identifier, + "source" => entry_source, + "occurred_at" => occurred_at, + "body" => parsed_file.content, + "kind" => entry_kind, + "created_at" => created_at, + "updated_at" => updated_at, + skip_local_sync: true + }).slice(*Entry.accepted_attributes) + + # handle metadata! + metadata_keys = (parsed_file.front_matter.keys - Entry.accepted_attributes) + if metadata_keys.any? + # if the user has specified a non Hash value, ie "metadata: foo", then + # throw a slightly easier to understand error here, instead of later on + # when we try to instantiate the Entry object & the error gets thrown there. + if entry_attributes["metadata"] && !entry_attributes["metadata"].is_a?(Hash) + raise "I expected the 'metadata' key on #{identifier} to be Hash, but something else is going on." + end + + entry_attributes["metadata"] ||= {} + metadata_keys.each do |mkey| + entry_attributes["metadata"][mkey] ||= parsed_file.front_matter[mkey] + end + end + + entry_attributes + end + + def add_entry!(entry_attributes) + identifier = entry_attributes["identifier"] + + # find or update the entry + entry = current_notebook.entries.find_by(identifier: identifier) + + if entry + entry.update!(entry_attributes) + else + entry = current_notebook.entries.create(entry_attributes) + end + + entry + end + + def render_stylesheet + # okay lets do the dumbest thing possible + # reload the file everytime!!!! + + scss_path = build_file_path("stylesheets/application.css.scss") + + rendered_stylesheet = nil + if File.exist?(scss_path) + load_path = File.join(current_notebook.import_path, "stylesheets") + + rendered_css = SassC::Engine.new(File.read(scss_path), { + filename: "application.css.scss", + syntax: :scss, + load_paths: [load_path], + }).render + + # there can only be ONE application.css + if to_delete = current_notebook.entries.find_by(identifier: "stylesheets/application.css") + puts "Destroying extraneous stylesheets/application.css, so it can be replaced." + to_delete.destroy + end + + rendered_stylesheet = current_notebook.entries.new + rendered_stylesheet.identifier = "stylesheets/application.css" + rendered_stylesheet.kind = :document + rendered_stylesheet.save! + + blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(rendered_css), + metadata: { analyzed: true }, + filename: "application.css") + # blob.analyze + rendered_stylesheet.files.create(blob_id: blob.id, created_at: blob.created_at) + end + + rendered_stylesheet + end +end diff --git a/app/models/entry_renderer.rb b/app/models/entry_renderer.rb index bc99ecef2..0d7f1836b 100644 --- a/app/models/entry_renderer.rb +++ b/app/models/entry_renderer.rb @@ -1,7 +1,7 @@ require 'task_list/filter' class EntryRenderer attr_accessor :entry, :output, :html - + # avail options: # todo_only: true # smart_punctuation: true @@ -33,6 +33,28 @@ def pipeline(opt = {}) end end + # used for rendering ERB but with access to helpers + class EntryContext + include ActionView::Context + include ActionView::Helpers + include ActionView::RoutingUrlFor + include Rails.application.routes.url_helpers + include UrlHelper + + attr_reader :entry + def initialize(entry) + @entry = entry + end + + def default_url_options + {format: "html"} + end + + def binding + super + end + end + # do we take an attribute? we're rendering an entry when was the last fucking time i rendered something other than a body? def render(opt = {}) @@ -58,6 +80,10 @@ def to_html(attribute_name = "body", opt = {}) if !attribute "" else + if entry.template? + attribute = ERB.new(attribute).result(EntryContext.new(entry).binding).html_safe + end + render_html(attribute, opt) end end diff --git a/app/views/static_site/timeline/hidden_entries.html.erb b/app/views/static_site/timeline/hidden_entries.html.erb index 9d2e9bee8..7599f2c97 100644 --- a/app/views/static_site/timeline/hidden_entries.html.erb +++ b/app/views/static_site/timeline/hidden_entries.html.erb @@ -3,7 +3,7 @@ <% @entries.each do |entry| %> <%= link_to entry.identifier, entry_path(entry) %> <% end %> - <%= link_to "archive", archive_path %> + <%= link_to "archive", archive_path(format: "html") %>