PageRenderTime 67ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/documatic/open_document_spreadsheet/template.rb

https://github.com/TuxmAL/documatic
Ruby | 259 lines | 152 code | 46 blank | 61 comment | 14 complexity | 13cd869f23cc676145d55d83073f0611 MD5 | raw file
  1. require 'rexml/document'
  2. require 'rexml/text'
  3. require 'rexml/attribute'
  4. require 'zip'
  5. require 'erb'
  6. require 'fileutils'
  7. module Documatic::OpenDocumentSpreadsheet
  8. class Template
  9. include ERB::Util
  10. attr_accessor :content
  11. attr_accessor :styles
  12. attr_accessor :jar
  13. # The raw contents of 'content.xml'.
  14. attr_accessor :content_raw
  15. # Compiled text, to be written to 'content.erb'
  16. attr_accessor :content_erb
  17. # RE_STYLES match positions
  18. STYLE_NAME = 1
  19. STYLE_TYPE = 2
  20. # RE_ERB match positions
  21. ROW_START = 1
  22. TYPE = 2
  23. ERB_CODE = 3
  24. ROW_END = 4
  25. # Abbrevs
  26. DSC = Documatic::OpenDocumentSpreadsheet::Component
  27. class << self
  28. def process_template(args, &block)
  29. if args[:options] && args[:options].template_file &&
  30. args[:options].output_file
  31. output_dir = File.dirname(args[:options].output_file)
  32. File.directory?(output_dir) || FileUtils.mkdir_p(output_dir)
  33. FileUtils.cp(args[:options].template_file, args[:options].output_file)
  34. template = self.new(args[:options].output_file)
  35. template.process :data => args[:data], :options => args[:options]
  36. template.save
  37. if block
  38. block.call(template)
  39. template.save
  40. end
  41. template.close
  42. else
  43. raise ArgumentError,
  44. 'Need to specify both :template_file and :output_file in options'
  45. end
  46. end
  47. end # class << self
  48. def initialize(filename)
  49. @filename = filename
  50. @jar = Zip::File.open(@filename)
  51. return true
  52. end
  53. def process(local_assigns = {})
  54. # Compile this template, if not compiled already.
  55. self.jar.find_entry('documatic/master') || self.compile
  56. # Process the main (body) content.
  57. @content = DSC.new( self.jar.read('documatic/master/content.erb') )
  58. @content.process(local_assigns)
  59. end
  60. def save
  61. # Gather all the styles from the partials, add them to the
  62. # master's styles. Put the body into the document.
  63. self.jar.get_output_stream('content.xml') do |f|
  64. f.write self.content.to_s
  65. end
  66. end
  67. def close
  68. # To get rid of an annoying message about corrupted files in OOCalc 3.2.0
  69. # we must remove the compiled content before we close our ODS file.
  70. self.jar.remove('documatic/master/content.erb')
  71. # Now we can safely close our document.
  72. self.jar.close
  73. end
  74. def compile
  75. # Read the raw files
  76. @content_raw = regularise_styles( self.jar.read('content.xml') )
  77. @content_erb = self.erbify(@content_raw)
  78. # Create 'documatic/master/' in zip file
  79. self.jar.find_entry('documatic/master') ||
  80. self.jar.mkdir('documatic/master')
  81. self.jar.get_output_stream('documatic/master/content.erb') do |f|
  82. f.write @content_erb
  83. end
  84. end
  85. protected
  86. # Change OpenDocument line breaks and tabs in the ERb code to
  87. # regular characters.
  88. def unnormalize(code)
  89. code = code.gsub(/<text:line-break\/>/, "\n")
  90. code = code.gsub(/<text:tab\/>/, "\t")
  91. return REXML::Text.unnormalize(code)
  92. end
  93. # Massage OpenDocument XML into ERb. (This is the heart of the compiler.)
  94. def erbify(code)
  95. # First gather all the ERb-related derived styles
  96. remaining = code
  97. styles = {'Ruby_20_Code' => 'Code', 'Ruby_20_Value' => 'Value',
  98. 'Ruby_20_Literal' => 'Literal'}
  99. re_styles = /<style:style style:name="([^"]+)" style:parent-style-name="Ruby_20_(Code|Value|Literal)" style:family="table-cell">/
  100. while remaining.length > 0
  101. md = re_styles.match remaining
  102. if md
  103. styles[md[STYLE_NAME]] = md[STYLE_TYPE]
  104. remaining = md.post_match
  105. else
  106. remaining = ""
  107. end
  108. end
  109. remaining = code
  110. result = String.new
  111. # Then make a RE that includes the ERb-related styles.
  112. # Match positions:
  113. #
  114. # 1. ROW_START Begin table row ?
  115. # 2. TYPE ERb text style type
  116. # 3. ERB_CODE ERb code
  117. # 4. ROW_END End table row (empty cells then end of row) ?
  118. #
  119. # "?": optional, might not occur every time
  120. re_erb = /(<table:table-row[^>]*>)?<table:table-cell [^>]*table:style-name="(#{styles.keys.join '|'})"[^>]*><text:p>([^<]*)<\/text:p><\/table:table-cell>(((<table:covered-table-cell[^\/>]*\/>)|(<table:table-cell[^\/>]*\/>))*<\/table:table-row>)?/
  121. # Then search for all text using those styles
  122. while remaining.length > 0
  123. md = re_erb.match remaining
  124. if md
  125. result += md.pre_match
  126. #match_code = false
  127. #match_row = false
  128. # if md[ROW_START] and md[ROW_END]
  129. # match_row=true
  130. # end
  131. skip_row=false
  132. #create cells, but dont append
  133. cells= case styles[md[TYPE]]
  134. when "Code" then
  135. skip_row=true
  136. "<% #{self.unnormalize md[ERB_CODE]} %>"
  137. when "Literal" then "<%= #{self.unnormalize md[ERB_CODE]} %>"
  138. when "Value" then "<%=cell (#{self.unnormalize md[ERB_CODE] }) %>" #let helper build the correct "cell"
  139. end
  140. #fist see if we should open row tag
  141. #
  142. #N.B - this assumes that there should be NO CELL VALUS after this one on the same row (will create invalid document)...
  143. if md[ROW_START] and not skip_row
  144. result+= md[ROW_START]
  145. end
  146. #then cell body
  147. result+=cells
  148. #then close
  149. if md[ROW_END] and not skip_row
  150. result += md[ROW_END]
  151. end
  152. remaining = md.post_match
  153. else # no further matches
  154. result += remaining
  155. remaining = ""
  156. end
  157. end
  158. return result
  159. end
  160. # OOo has a queer way of storing style information for cells. In
  161. # some cases it is in the cell's attribute "table:style-name", but
  162. # the default style for cells is also stored in the columns
  163. # section at the beginning of each sheet. So there's no way of
  164. # knowing in advance whether a cell will have its style specified
  165. # or whether it has to be implied from the column definitions.
  166. # This method regularises the cell styles: it takes the style
  167. # definitions from each sheet's column definitions and applies
  168. # them to any cells where the style is not specified. The result
  169. # is a still-valid XML document but with explicit styles on each
  170. # cell. This makes the document easier to compile.
  171. def regularise_styles(content_raw)
  172. doc = REXML::Document.new(content_raw)
  173. # Get the default column types from all the sheets (tables) in
  174. # the workbook
  175. num_tables = doc.root.elements.to_a('//office:body/*/table:table').length
  176. (1 .. num_tables).to_a.each do |tnum|
  177. col_types = []
  178. cols = doc.root.elements.to_a("//table:table[#{tnum}]/table:table-column")
  179. cols.each do |col|
  180. (0 ... (col.attributes['table:number-columns-repeated'] ||
  181. 1).to_i).to_a.each do
  182. col_types << col.attributes['table:default-cell-style-name']
  183. end
  184. end # each column
  185. # Get the number of rows for each table
  186. num_rows = doc.root.elements.to_a("//table:table[#{tnum}]/table:table-row").length
  187. # Go through each row and process its cells
  188. (1 .. num_rows).to_a.each do |rnum|
  189. # The cells are both <table:table-cell> and
  190. # <table:covered-table-cell>
  191. cells = doc.root.elements.to_a(<<-END
  192. //table:table[#{tnum}]/table:table-row[#{rnum}]/(table:table-cell | table:covered-table-cell)
  193. END
  194. )
  195. # Keep track of the column number, for formatting purposes
  196. # (c.f. col_types)
  197. col_num = 0
  198. cells.each do |cell|
  199. # Only need to explicitly format the <table:table-cell>s
  200. if cell.name == 'table-cell'
  201. cell.attributes['table:style-name'] ||= col_types[col_num]
  202. end
  203. # Advance the column number, based on the columns spanned
  204. # by the cell
  205. col_num += (cell.attributes['table:number-columns-repeated'] ||
  206. 1).to_i
  207. end
  208. end # each row
  209. end # each table
  210. return doc.to_s
  211. end
  212. end
  213. end