Class: Brief::Document

Inherits:
Object
  • Object
show all
Includes:
Attachments, FrontMatter, Rendering, Templating
Defined in:
lib/brief/document.rb,
lib/brief/document/rendering.rb,
lib/brief/document/attachments.rb,
lib/brief/document/front_matter.rb

Defined Under Namespace

Modules: Attachments, FrontMatter, Rendering, Templating Classes: ContentExtractor, Section, Structure, Transformer

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Attachments

#attachment_paths, #has_attachments?, #render_attachments

Methods included from Templating

#generate_content

Methods included from FrontMatter

#data=, #frontmatter_line_count

Methods included from Rendering

#script_contents, #script_preamble, #to_html, #unwrapped_html

Constructor Details

#initialize(path, options = {}) ⇒ Document



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/brief/document.rb', line 13

def initialize(path, options = {})
  if path.respond_to?(:key?) && options.empty?
    @frontmatter = path.to_mash
  else
    @path = Pathname(path)
  end

  @options = options.to_mash

  if @path && self.path.exist?
    @raw_content = self.path.read
    load_frontmatter
  elsif options[:contents]
    @raw_content = options[:contents]
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(meth, *args, &block) ⇒ Object



320
321
322
323
324
325
326
# File 'lib/brief/document.rb', line 320

def method_missing(meth, *args, &block)
  if data.respond_to?(meth)
    data.send(meth, *args, &block)
  else
    super
  end
end

Instance Attribute Details

#contentObject

Returns the value of attribute content.



11
12
13
# File 'lib/brief/document.rb', line 11

def content
  @content
end

#frontmatterObject

Returns the value of attribute frontmatter.



11
12
13
# File 'lib/brief/document.rb', line 11

def frontmatter
  @frontmatter
end

#optionsObject

Returns the value of attribute options.



11
12
13
# File 'lib/brief/document.rb', line 11

def options
  @options
end

#pathObject

Returns the value of attribute path.



11
12
13
# File 'lib/brief/document.rb', line 11

def path
  @path
end

#raw_contentObject

Returns the value of attribute raw_content.



11
12
13
# File 'lib/brief/document.rb', line 11

def raw_content
  @raw_content
end

Class Method Details

.from_contents(content, frontmatter, &block) ⇒ Object



8
9
# File 'lib/brief/document.rb', line 8

def self.from_contents(content, frontmatter, &block)
end

Instance Method Details

#at(*args, &block) ⇒ Object

Returns a Nokogiri::HTML::Element



194
195
196
# File 'lib/brief/document.rb', line 194

def at(*args, &block)
  parser.send(:at, *args, &block)
end

#attachmentsObject



123
124
125
# File 'lib/brief/document.rb', line 123

def attachments
  Array(data.attachments)
end

#briefcaseObject



137
138
139
# File 'lib/brief/document.rb', line 137

def briefcase
  (@briefcase_root && Brief.cases[@briefcase_root.basename.to_s]) || Brief.case(true)
end

#cloneObject



34
35
36
# File 'lib/brief/document.rb', line 34

def clone
  new(path, options)
end

#combined_data_and_contentObject



110
111
112
113
# File 'lib/brief/document.rb', line 110

def combined_data_and_content
  return content if data.nil? || data.empty?
  frontmatter.to_hash.to_yaml + "---\n\n#{ content }"
end

#content_hashObject



54
55
56
# File 'lib/brief/document.rb', line 54

def content_hash
  Digest::MD5.hexdigest(@content.to_s)
end

#content_stale?Boolean



62
63
64
# File 'lib/brief/document.rb', line 62

def content_stale?
  content_hash != file_hash
end

#css(*args, &block) ⇒ Object

Shortcut for querying the rendered HTML by css selectors.

This will allow for model data attributes to be pulled from the document contents.

Returns a Nokogiri::HTML::Element



189
190
191
# File 'lib/brief/document.rb', line 189

def css(*args, &block)
  parser.send(:css, *args, &block)
end

#dataObject



115
116
117
# File 'lib/brief/document.rb', line 115

def data
  frontmatter
end

#documentObject



30
31
32
# File 'lib/brief/document.rb', line 30

def document
  self
end

#document_typeObject



253
254
255
# File 'lib/brief/document.rb', line 253

def document_type
  options.fetch(:type) { document_type! }
end

#document_type!Object



257
258
259
260
261
# File 'lib/brief/document.rb', line 257

def document_type!
  existing = data && data.type
  return existing if existing
  parent_folder_name.try(:singularize)
end

#exist?Boolean



234
235
236
# File 'lib/brief/document.rb', line 234

def exist?
  path && path.exist?
end

#extensionObject



220
221
222
# File 'lib/brief/document.rb', line 220

def extension
  path.extname
end

#extract_content(*args) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/brief/document.rb', line 198

def extract_content(*args)
  options = args.extract_options!
  args    = options.delete(:args) if options.is_a?(Hash) && options.key?(:args)

  case
  when options.empty? && args.length == 1 && args.first.is_a?(String)
    results = css(args.first)
    results = results.first if results.length > 1 && args.first.match(/:first-of-type/)
    results.try(:text).to_s
  else
    binding.pry
  end
end

#file_hashObject



58
59
60
# File 'lib/brief/document.rb', line 58

def file_hash
  Digest::MD5.hexdigest(path.read.to_s)
end

#fragmentObject



312
313
314
# File 'lib/brief/document.rb', line 312

def fragment
  @fragment ||= Nokogiri::HTML.fragment(to_raw_html)
end

#has_sections?Boolean



141
142
143
# File 'lib/brief/document.rb', line 141

def has_sections?
  model_class.section_mappings.length > 0
end

#in_briefcase(briefcase) ⇒ Object



127
128
129
130
131
132
133
134
135
# File 'lib/brief/document.rb', line 127

def in_briefcase(briefcase)
  @briefcase_root = briefcase.root

  unless Brief::Util.ensure_child_path(briefcase.docs_path, path)
    raise 'Invalid document path'
  end

  self
end

#include_attachments?Boolean



119
120
121
# File 'lib/brief/document.rb', line 119

def include_attachments?
  attachments.length > 0
end

#inspectObject



46
47
48
# File 'lib/brief/document.rb', line 46

def inspect
  "#{ model_class }.at_path(#{relative_path})"
end

#model_attributesObject



224
225
226
227
228
# File 'lib/brief/document.rb', line 224

def model_attributes
  (data || {}).to_hash
    .merge(path: path, document: self)
    .reverse_merge(type: document_type)
end

#model_classObject



238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/brief/document.rb', line 238

def model_class
  case
  when @model_class
    @model_class
  when briefcase
    briefcase.model_class_for(self)
  when data && data.type
    Brief::Model.for_type(data.type)
  when parent_folder_name.length > 0
    Brief::Model.for_folder_name(parent_folder_name)
  else
    raise 'Could not determine the model class to use for this document. Specify the type, or put it in a folder that maps to the correct type.'
  end
end

#model_instance_registered?Boolean

Each model class tracks the instances of the models created and ensures that there is a 1-1 relationship between a document path and the model.



270
271
272
273
274
# File 'lib/brief/document.rb', line 270

def model_instance_registered?
  model_class && model_class.models.any? do |model|
    model.path == path
  end
end

#parent_folder_nameObject



263
264
265
# File 'lib/brief/document.rb', line 263

def parent_folder_name
  path.parent.basename.to_s.downcase
end

#parserObject

The Parser wraps the rendered HTML in a nokogiri element so we can easily manipulate it. Prior to doing so, we use the structure analyzer to build more metadata into the markup



291
292
293
294
295
296
297
298
299
# File 'lib/brief/document.rb', line 291

def parser
  @parser ||= begin
                structure.prescan

                structure.create_wrappers.tap do |f|
                  transformer_for(f).all
                end
              end
end

#raw=(val) ⇒ Object



66
67
68
69
70
71
# File 'lib/brief/document.rb', line 66

def raw= val
  @raw_set = true
  @raw_content = val
  #document.load_frontmatter
  @raw_content
end

#refresh!Object



88
89
90
91
92
93
94
95
96
97
# File 'lib/brief/document.rb', line 88

def refresh!
  @content = nil
  @raw_content = path.read
  @frontmatter = nil
  @raw_frontmatter = nil
  @refreshing = true
  @content_hash = nil
  load_frontmatter
  true
end

#relative_pathObject



50
51
52
# File 'lib/brief/document.rb', line 50

def relative_path
  briefcase.present? ? path.relative_path_from(briefcase.docs_path) : path
end

#relative_path_identifierObject



212
213
214
215
216
217
218
# File 'lib/brief/document.rb', line 212

def relative_path_identifier
  if Brief.case
    path.relative_path_from(Brief.case.root)
  else
    path.to_s
  end
end

#respond_to?(*args) ⇒ Boolean



276
277
278
279
# File 'lib/brief/document.rb', line 276

def respond_to?(*args)
  method = args.first
  super || (data && data.respond_to?(method)) || (data && data.key?(method))
end

#saveObject



77
78
79
80
81
82
83
84
85
86
# File 'lib/brief/document.rb', line 77

def save
  if set_raw?
    file_contents = raw_content
  else
    file_contents = combined_data_and_content
  end

  path.open('w') {|fh| fh.write(file_contents) }
  refresh!
end

#save!Object



99
100
101
102
103
104
105
106
107
108
# File 'lib/brief/document.rb', line 99

def save!
  if set_raw?
    file_contents = raw_content
  else
    file_contents = combined_data_and_content
  end

  path.open('w+') {|fh| fh.write(file_contents) }
  refresh!
end

#section_headingsObject



145
146
147
# File 'lib/brief/document.rb', line 145

def section_headings
  sections.keys
end

#sectionsObject



158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/brief/document.rb', line 158

def sections
  mappings = model_class.section_mappings

  @sections = {}.to_mash

  mappings.each do |name, mapping|
    fragment = css("section[data-heading='#{name}']").first
    @sections[name.parameterize.downcase.underscore] = Brief::Document::Section.new(name, fragment, mapping)
  end

  @sections
end

#sections_dataObject



149
150
151
152
153
154
155
156
# File 'lib/brief/document.rb', line 149

def sections_data
  section_headings.reduce({}) do |memo, heading|
    section = sections.send(heading)
    items = section.items rescue nil
    memo[heading] = items if items
    memo
  end
end

#set_raw?Boolean



73
74
75
# File 'lib/brief/document.rb', line 73

def set_raw?
  !!@raw_set
end

#structureObject

The structure analyzer of the document is responsible for grouping the content under the headings by wrapping them in divs, and creating relationships between the nodes. This is what lets us provide an easy iteration API on top of the parsed document



285
286
287
# File 'lib/brief/document.rb', line 285

def structure
  @structure_analyzer ||= Brief::Document::Structure.new(fragment, raw_content.lines.to_a)
end

#titleObject



38
39
40
# File 'lib/brief/document.rb', line 38

def title
  (data && data.title) || css('h1:first-of-type').text || path.to_s.split("/").last.gsub(/\..*/,'')
end

#to_modelObject



230
231
232
# File 'lib/brief/document.rb', line 230

def to_model
  model_class.try(:new, model_attributes)
end

#to_sObject



42
43
44
# File 'lib/brief/document.rb', line 42

def to_s
  "#{ model_class }.at_path(#{relative_path})"
end

#transformer_for(doc_fragment = nil) ⇒ Object

The transformer is responsible for performing content modifications on the rendered document. This is useful for supporting extensions that are driven by the markdown language.

TODO: This is hidden behind a feature flag, and requires the document to have metadata that specifies transform = true



307
308
309
310
# File 'lib/brief/document.rb', line 307

def transformer_for(doc_fragment=nil)
  doc_fragment ||= fragment
  @transformer ||= Brief::Document::Transformer.new(doc_fragment, self)
end

#typeObject



316
317
318
# File 'lib/brief/document.rb', line 316

def type
  document_type
end