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

Returns a new instance of 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



316
317
318
319
320
321
322
# File 'lib/brief/document.rb', line 316

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



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

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

#attachmentsObject



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

def attachments
  Array(data.attachments)
end

#briefcaseObject



133
134
135
# File 'lib/brief/document.rb', line 133

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

#combined_data_and_contentObject



106
107
108
109
# File 'lib/brief/document.rb', line 106

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

#content_hashObject



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

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

#content_stale?Boolean

Returns:

  • (Boolean)


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

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



185
186
187
# File 'lib/brief/document.rb', line 185

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

#dataObject



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

def data
  frontmatter
end

#documentObject



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

def document
  self
end

#document_typeObject



249
250
251
# File 'lib/brief/document.rb', line 249

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

#document_type!Object



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

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

#exist?Boolean

Returns:

  • (Boolean)


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

def exist?
  path && path.exist?
end

#extensionObject



216
217
218
# File 'lib/brief/document.rb', line 216

def extension
  path.extname
end

#extract_content(*args) ⇒ Object



194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/brief/document.rb', line 194

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



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

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

#fragmentObject



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

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

#has_sections?Boolean

Returns:

  • (Boolean)


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

def has_sections?
  model_class.section_mappings.length > 0
end

#in_briefcase(briefcase) ⇒ Object



123
124
125
126
127
128
129
130
131
# File 'lib/brief/document.rb', line 123

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

Returns:

  • (Boolean)


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

def include_attachments?
  attachments.length > 0
end

#inspectObject



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

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

#model_attributesObject



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

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

#model_classObject



234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/brief/document.rb', line 234

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.

Returns:

  • (Boolean)


266
267
268
269
270
# File 'lib/brief/document.rb', line 266

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

#parent_folder_nameObject



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

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



287
288
289
290
291
292
293
294
295
# File 'lib/brief/document.rb', line 287

def parser
  @parser ||= begin
                structure.prescan

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

#raw=(val) ⇒ Object



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

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

#refresh!Object



84
85
86
87
88
89
90
91
92
93
# File 'lib/brief/document.rb', line 84

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

#relative_pathObject



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

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

#relative_path_identifierObject



208
209
210
211
212
213
214
# File 'lib/brief/document.rb', line 208

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

#respond_to?(*args) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#saveObject



73
74
75
76
77
78
79
80
81
82
# File 'lib/brief/document.rb', line 73

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



95
96
97
98
99
100
101
102
103
104
# File 'lib/brief/document.rb', line 95

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



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

def section_headings
  sections.keys
end

#sectionsObject



154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/brief/document.rb', line 154

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



145
146
147
148
149
150
151
152
# File 'lib/brief/document.rb', line 145

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

Returns:

  • (Boolean)


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

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



281
282
283
# File 'lib/brief/document.rb', line 281

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

#titleObject



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

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

#to_modelObject



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

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

#to_sObject



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

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



303
304
305
306
# File 'lib/brief/document.rb', line 303

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

#typeObject



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

def type
  document_type
end