1 # Author: David Priest & David Goodger
2 # Contact: priest@sfu.ca
5 # Copyright: This module has been placed in the public domain.
8 Directive for CSV (comma-separated values) Tables.
14 from docutils
import nodes
, statemachine
, utils
15 from docutils
.utils
import SystemMessagePropagation
16 from docutils
.transforms
import references
17 from docutils
.parsers
.rst
import directives
26 except NameError: # Python 2.2 & 2.1 compatibility
31 class DocutilsDialect(csv
.Dialect
):
36 skipinitialspace
= True
38 quoting
= csv
.QUOTE_MINIMAL
40 def __init__(self
, options
):
41 if options
.has_key('delim'):
42 self
.delimiter
= str(options
['delim'])
43 if options
.has_key('quote'):
44 self
.quotechar
= str(options
['quote'])
45 if options
.has_key('escape'):
46 self
.doublequote
= False
47 self
.escapechar
= str(options
['escape'])
48 csv
.Dialect
.__init
__(self
)
51 class HeaderDialect(csv
.Dialect
):
53 """CSV dialect to use for the "header" option data."""
59 skipinitialspace
= True
61 quoting
= csv
.QUOTE_MINIMAL
64 def csv_table(name
, arguments
, options
, content
, lineno
,
65 content_offset
, block_text
, state
, state_machine
):
67 title
, messages
= make_title(arguments
, state
, lineno
)
69 csv_data
, source
= get_csv_data(
70 name
, options
, content
, lineno
, block_text
, state
, state_machine
)
71 table_head
, max_header_cols
= process_header_option(
72 options
, state_machine
, lineno
)
73 rows
, max_cols
= parse_csv_data_into_rows(
74 csv_data
, DocutilsDialect(options
), source
, options
)
75 max_cols
= max(max_cols
, max_header_cols
)
76 header_rows
= options
.get('header-rows', 0) # default 0
77 table_head
.extend(rows
[:header_rows
])
78 table_body
= rows
[header_rows
:]
80 error
= state_machine
.reporter
.error(
81 '"%s" directive requires table body content.' % name
,
82 nodes
.literal_block(block_text
, block_text
), line
=lineno
)
84 col_widths
= get_col_widths(
85 max_cols
, name
, options
, lineno
, block_text
, state_machine
)
86 extend_short_rows_with_empty_cells(max_cols
, (table_head
, table_body
))
87 except SystemMessagePropagation
, detail
:
88 return [detail
.args
[0]]
89 except csv
.Error
, detail
:
90 error
= state_machine
.reporter
.error(
91 'Error with CSV data in "%s" directive:\n%s' % (name
, detail
),
92 nodes
.literal_block(block_text
, block_text
), line
=lineno
)
94 table
= (col_widths
, table_head
, table_body
)
95 table_node
= state
.build_table(table
, content_offset
)
96 if options
.has_key('class'):
97 table_node
.set_class(options
['class'])
99 table_node
.insert(0, title
)
100 return [table_node
] + messages
102 def make_title(arguments
, state
, lineno
):
104 title_text
= arguments
[0]
105 text_nodes
, messages
= state
.inline_text(title_text
, lineno
)
106 title
= nodes
.title(title_text
, '', *text_nodes
)
110 return title
, messages
112 def get_csv_data(name
, options
, content
, lineno
, block_text
,
113 state
, state_machine
):
114 if content
: # CSV data is from directive content
115 if options
.has_key('file') or options
.has_key('url'):
116 error
= state_machine
.reporter
.error(
117 '"%s" directive may not both specify an external file and '
118 'have content.' % name
,
119 nodes
.literal_block(block_text
, block_text
), line
=lineno
)
120 raise SystemMessagePropagation(error
)
121 source
= content
.source(0)
123 elif options
.has_key('file'): # CSV data is from an external file
124 if options
.has_key('url'):
125 error
= state_machine
.reporter
.error(
126 'The "file" and "url" options may not be simultaneously '
127 'specified for the "%s" directive.' % name
,
128 nodes
.literal_block(block_text
, block_text
), line
=lineno
)
129 raise SystemMessagePropagation(error
)
130 source_dir
= os
.path
.dirname(
131 os
.path
.abspath(state
.document
.current_source
))
132 source
= os
.path
.normpath(os
.path
.join(source_dir
, options
['file']))
133 source
= utils
.relative_path(None, source
)
135 csv_file
= open(source
, 'rb')
137 csv_data
= csv_file
.read().splitlines()
140 except IOError, error
:
141 severe
= state_machine
.reporter
.severe(
142 'Problems with "%s" directive path:\n%s.' % (name
, error
),
143 nodes
.literal_block(block_text
, block_text
), line
=lineno
)
144 raise SystemMessagePropagation(severe
)
145 elif options
.has_key('url'): # CSV data is from a URL
147 severe
= state_machine
.reporter
.severe(
148 'Problems with the "%s" directive and its "url" option: '
149 'unable to access the required functionality (from the '
150 '"urllib2" module).' % name
,
151 nodes
.literal_block(block_text
, block_text
), line
=lineno
)
152 raise SystemMessagePropagation(severe
)
153 source
= options
['url']
155 csv_data
= urllib2
.urlopen(source
).read().splitlines()
156 except (urllib2
.URLError
, IOError, OSError, ValueError), error
:
157 severe
= state_machine
.reporter
.severe(
158 'Problems with "%s" directive URL "%s":\n%s.'
159 % (name
, options
['url'], error
),
160 nodes
.literal_block(block_text
, block_text
), line
=lineno
)
161 raise SystemMessagePropagation(severe
)
163 error
= state_machine
.reporter
.warning(
164 'The "%s" directive requires content; none supplied.' % (name
),
165 nodes
.literal_block(block_text
, block_text
), line
=lineno
)
166 raise SystemMessagePropagation(error
)
167 return csv_data
, source
169 def process_header_option(options
, state_machine
, lineno
):
170 source
= state_machine
.get_source(lineno
- 1)
173 if options
.has_key('header'): # separate table header in option
174 rows
, max_header_cols
= parse_csv_data_into_rows(
175 options
['header'].split('\n'), HeaderDialect(), source
, options
)
176 table_head
.extend(rows
)
177 return table_head
, max_header_cols
179 def parse_csv_data_into_rows(csv_data
, dialect
, source
, options
):
180 csv_reader
= csv
.reader(csv_data
, dialect
=dialect
)
183 for row
in csv_reader
:
186 cell_data
= (0, 0, 0, statemachine
.StringList(cell
.splitlines(),
188 row_data
.append(cell_data
)
189 rows
.append(row_data
)
190 max_cols
= max(max_cols
, len(row
))
191 return rows
, max_cols
193 def get_col_widths(max_cols
, name
, options
, lineno
, block_text
,
195 if options
.has_key('widths'):
196 col_widths
= options
['widths']
197 if len(col_widths
) != max_cols
:
198 error
= state_machine
.reporter
.error(
199 '"%s" widths do not match the number of columns in table (%s).'
201 nodes
.literal_block(block_text
, block_text
), line
=lineno
)
202 raise SystemMessagePropagation(error
)
204 col_widths
= [100 / max_cols
] * max_cols
207 def extend_short_rows_with_empty_cells(columns
, parts
):
210 if len(row
) < columns
:
211 row
.extend([(0, 0, 0, [])] * (columns
- len(row
)))
213 def single_char_or_unicode(argument
):
214 char
= directives
.unicode_code(argument
)
216 raise ValueError('%r invalid; must be a single character or '
217 'a Unicode code' % char
)
220 def single_char_or_whitespace_or_unicode(argument
):
221 if argument
== 'tab':
223 elif argument
== 'space':
226 char
= single_char_or_unicode(argument
)
229 def positive_int(argument
):
230 value
= int(argument
)
232 raise ValueError('negative or zero value; must be positive')
235 def positive_int_list(argument
):
237 entries
= argument
.split(',')
239 entries
= argument
.split()
240 return [positive_int(entry
) for entry
in entries
]
242 csv_table
.arguments
= (0, 1, 1)
243 csv_table
.options
= {'header-rows': directives
.nonnegative_int
,
244 'header': directives
.unchanged
,
245 'widths': positive_int_list
,
246 'file': directives
.path
,
247 'url': directives
.path
,
248 'class': directives
.class_option
,
249 # field delimiter char
250 'delim': single_char_or_whitespace_or_unicode
,
251 # text field quote/unquote char:
252 'quote': single_char_or_unicode
,
253 # char used to escape delim & quote as-needed:
254 'escape': single_char_or_unicode
,}
255 csv_table
.content
= 1
257 directives
.register_directive('csvtable', csv_table
)