adamh-html_namespacing 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Manifest ADDED
@@ -0,0 +1,12 @@
1
+ README.rdoc
2
+ Rakefile
3
+ test/c_extension_test.rb
4
+ test/test_helper.rb
5
+ ext/html_namespacing/extconf.rb
6
+ ext/html_namespacing/html_namespacing.h
7
+ ext/html_namespacing/html_namespacing.c
8
+ ext/html_namespacing/html_namespacing_ext.c
9
+ lib/html_namespacing.rb
10
+ lib/html_namespacing/plugin/rails.rb
11
+ Manifest
12
+ html_namespacing.gemspec
data/README.rdoc ADDED
@@ -0,0 +1,274 @@
1
+ = HTML Namespacing
2
+
3
+ HTML Namespacing automatically adds HTML class attributes to partial HTML
4
+ code.
5
+
6
+ == Installing
7
+
8
+ gem install adamh-html_namespacing --source http://gems.github.com
9
+
10
+ == Using
11
+
12
+ HTML Namespacing can be used on its own for a snippet of code:
13
+
14
+ require 'rubygems'
15
+ require 'html_namespacing'
16
+
17
+ html = '<p>Here is some <em>HTML</em>!</p>'
18
+
19
+ # Returns '<p class="foo">Here is some <em>HTML</em>!</p>'
20
+ namespaced = HtmlNamespacing::add_namespace_to_html(html, 'foo')
21
+
22
+ == Details
23
+
24
+ HTML namespacing expects valid, properly-nested HTML (not XML). It has no
25
+ effect on doctype tags, XML declaration, comments, and the HTML tags which do
26
+ not support the <tt>class</tt> attribute: <tt>html</tt>, <tt>head</tt>,
27
+ <tt>base</tt>, <tt>meta</tt>, <tt>title</tt>, <tt>link</tt>, <tt>script</tt>,
28
+ <tt>noscript</tt>, <tt>style</tt>.
29
+
30
+ Only the root tags of the partial HTML will be namespaced. For instance, all
31
+ <tt>p</tt> tags in this example will have a class added, but no other tags:
32
+
33
+ <p>This is a root tag. <b>This tag is nested.</b></p>
34
+ <p>This is another root tag.</p>
35
+
36
+ Remember, <tt>html</tt> (and other HTML tags which do not allow the
37
+ <tt>class</tt> attribute) will be ignored. This HTML will pass through
38
+ unchanged:
39
+
40
+ <?xml version="1.0"?>
41
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
42
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
43
+ <!-- Here is a comment -->
44
+ <html>
45
+ <head>
46
+ <title>Hello</title>
47
+ </head>
48
+ <body>
49
+ <p>Blah blah blah</p>
50
+ </body>
51
+ </html>
52
+
53
+ Though the actual namespacing functions are written in pure C with no
54
+ dependencies, only Ruby bindings are available at this time.
55
+
56
+ == Integrating in your Project
57
+
58
+ More likely, you want to integrate HTML namespacing into a framework. Here is
59
+ a Rails example:
60
+
61
+ In <tt>config/environment.rb</tt>:
62
+
63
+ config.gem 'html_namespacing'
64
+
65
+ In <tt>config/initializers/html_namespacing.rb</tt>:
66
+
67
+ HtmlNamespacing::Plugin::Rails.install
68
+
69
+ Now, all templates will have HTML namespacing applied. For instance, with a
70
+ <tt>views/foos/show.html.erb</tt> like this:
71
+
72
+ <p>
73
+ <b>Name:</b>
74
+ <%=h @foo.name %>
75
+ </p>
76
+
77
+ <%= render(:partial => 'details', :locals => { :foo => @foo }) %>
78
+
79
+ <%= link_to 'Edit', foo_edit_path(@foo) %> |
80
+ <%= link_to 'Back', foos_path %>
81
+
82
+ The following HTML might be generated (depending on the <tt>_details</tt>
83
+ partial and the data in the actual <tt>Foo</tt> object):
84
+
85
+ <p class="foos-show">
86
+ <b>Name:</b>
87
+ Foo
88
+ </p>
89
+
90
+ <p class="foos-_details foos-show">
91
+ <b>Description:</b>
92
+ Bar
93
+ </p>
94
+
95
+ <a href="/foos/1/edit" class="hellos-show">Edit</a> |
96
+ <a href="/foos" class="hellos-show">Back</a>
97
+
98
+ Rails will integrate with all HTML templates, regardless of their
99
+ implementation; feel free to use Haml.
100
+
101
+ === Options
102
+
103
+ Available options to <tt>install</tt> are:
104
+
105
+ <tt>:template_to_namespace_callback</tt>: Ruby lambda function which accepts an
106
+ <tt>ActionView::Template</tt> and returns an HTML namespacing string. The
107
+ default is:
108
+
109
+ lambda { |template| template.path =~ /^([^\.]+)/ && $1.gsub('/', '-') || nil }
110
+
111
+ If the callback returns <tt>nil</tt>, HTML namespacing will not be applied.
112
+
113
+ == Why?
114
+
115
+ HTML namespacing gives huge benefits when writing CSS and JavaScript:
116
+ especially if the CSS and JavaScript components are automatically namespaced
117
+ in a similar manner.
118
+
119
+ Imagine we have the following namespaced HTML:
120
+
121
+ <div class="foos-show">
122
+ <p>In the show partial</p>
123
+ <div class="foos-_details">
124
+ <p>In the details partial</p>
125
+ <div class="foos-_description">
126
+ <p>In the description</p>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ === CSS Namespacing
132
+
133
+ We can set three CSS rules:
134
+
135
+ .foos-show p { font-size: 1.1em; }
136
+ .foos-_details p { font-weight: bold; }
137
+ .foos-_description p { font-style: italic; }
138
+
139
+ In such an example, the top paragraph would be large, the second would be large
140
+ and bold, and the third would be large, bold, and italic.
141
+
142
+ The benefit comes when automating these namespaces. For instance, if the CSS
143
+ rules were placed in <tt>stylesheets/views/foos/show.css</tt>,
144
+ <tt>stylesheets/views/foos/_details.css</tt>, and
145
+ <tt>stylesheets/views/foos/_description.css</tt>, respectively, and a
146
+ preprocessor inserted the namespace before (or within) every rule in the file,
147
+ all CSS namespacing would be done for free, so you would be able to write:
148
+
149
+ <tt>stylesheets/views/foos/show.css</tt>:
150
+
151
+ p { font-size: 1.1em; }
152
+
153
+ <tt>stylesheets/views/foos/_details.css</tt>:
154
+
155
+ p { font-weight: bold; }
156
+
157
+ <tt>stylesheets/views/foos/_description.css</tt>:
158
+
159
+ p { font-style: italic; }
160
+
161
+ Thus, the namespaces would never need to be explicitly mentioned. The
162
+ framework used to generate such CSS is left (for now) as an exercise to the
163
+ reader.
164
+
165
+ === jQuery Namespacing
166
+
167
+ Insert the following before all other JavaScript:
168
+
169
+ $(document).ready(function() {
170
+ $.NS = {};
171
+
172
+ var SPLIT_REGEX = /\s+/;
173
+
174
+ function populate_NS_from_node(node) {
175
+ var node_stack = [];
176
+ node_stack.push(node);
177
+
178
+ while (node_stack.length) {
179
+ var cur_node = node_stack.pop();
180
+
181
+ // Populate entry in $.NS
182
+ var class_name_list = node.className;
183
+ var class_names = class_name_list.split(SPLIT_REGEX);
184
+ for (var i = 0; i < class_names.length; i++) {
185
+ var class_name = class_names[i];
186
+ if (!$.NS[class_name]) {
187
+ $.NS[class_name] = [cur_node];
188
+ } else {
189
+ $.NS[class_name].push(cur_node);
190
+ }
191
+ }
192
+
193
+ // "recurse" to the children, so they are handled in document order
194
+ var children = cur_node.childNodes;
195
+ for (var j = children.length - 1; j >= 0; j--) {
196
+ var subnode = children[j];
197
+ node_stack.push(subnode);
198
+ }
199
+ }
200
+ }
201
+
202
+ populate_NS_from_node(document.getElementsByTagName('body')[0]);
203
+ });
204
+
205
+ (The CPU cost of this code is small but nonzero, as it precomputes many
206
+ selectors. On a 75kb page with a 2.5Ghz computer, it might take 75ms with
207
+ Firebug profiling turned on. This may or may not be faster than writing each
208
+ selector by hand.)
209
+
210
+ Afterwards, if you set up a JavaScript framework similar to the CSS framework
211
+ above, you can easily namespace JavaScript behavior:
212
+
213
+ <tt>javascripts/onload/foos/_description.js</tt>:
214
+
215
+ $(document).ready(function() {
216
+ var $NS = $.NS['foos-_description']; // auto-generated line at top of file
217
+ $NS.find('p').click(function() { alert("Yup, you clicked!"); });
218
+ });
219
+
220
+ In the above file, if every line but the third is automatically generated based
221
+ on the file's location (an exercise, for now, left to the reader), the third
222
+ line can be written in full confidence that only paragraphs within the
223
+ <tt>_description</tt> partial will have this jQuery code bound. The end result?
224
+ You can write code specific to one Rails partial without actually writing the
225
+ partial's name anywhere, and you will not need to rewrite any selectors if you
226
+ rename or move the partial.
227
+
228
+ == Tips and Tricks
229
+
230
+ You may find that HTML namespacing works best when each HTML partial is wrapped
231
+ in its own <tt>div</tt> tag. Both CSS's child selectors and jQuery's <tt>find()</tt> function
232
+ will ordinarily ignore the element with the namespace class, only selecting
233
+ sub-elements. For instance, with this HTML:
234
+
235
+ <div class="foos-show">
236
+ <p>Hello</p>
237
+ </div>
238
+
239
+ If you wrote the following CSS:
240
+
241
+ .foos-show div { font-weight: bold; }
242
+
243
+ Or the following JavaScript (following the <tt>$NS</tt> example above):
244
+
245
+ var $div = $NS.find('div');
246
+
247
+ Nothing will be selected, because the <tt>div</tt> element is not a child. You
248
+ would need the following CSS:
249
+
250
+ div.foos-show { font-weight: bold; }
251
+
252
+ Or the following JavaScript:
253
+
254
+ var $div = $NS.filter('div');
255
+
256
+ Also, watch out for nesting: selectors with the <tt>foos-show</tt> namespace
257
+ will match anything inside any partials rendered by
258
+ <tt>foos/show.html.erb</tt>. As a rule of thumb to circumvent this problem: the
259
+ wider the namespaces's scope, the less CSS and JavaScript you should write
260
+ which depends on it. (Rule of thumb: use sub-partials very liberally.)
261
+
262
+ These and similar strategies should be considered when building your automated
263
+ HTML namespacing framework.
264
+
265
+ == License
266
+
267
+ I believe in software freedom, not any abomination thereof. This project is
268
+ released under the Public Domain, meaning I relinquish my copyright (to any
269
+ extend the law allows) and grant you all rights to use, modify, sell, or
270
+ otherwise take advantage of my software.
271
+
272
+ However, I do kindly request that, as a favor, you refrain from using my
273
+ software as part of an evil plan involving velociraptors and mind-controlling
274
+ robots (even though I would not be legally entitled to sue you for doing so).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ Echoe.new('html_namespacing', '0.0.1') do |p|
6
+ p.author = 'Adam Hooper'
7
+ p.email = 'adam@adamhooper.com'
8
+ p.summary = 'Automatic HTML namespacing'
9
+ p.description = 'Inserts "class=" attributes within snippets of HTML so CSS and JavaScript can use automatic scopes'
10
+ p.url = 'http://github.com/adamh/html_namespacing'
11
+ p.rdoc_files = ['README.rdoc'] # echoe seems to gather everything twice
12
+ end
@@ -0,0 +1,6 @@
1
+ require 'mkmf'
2
+
3
+ name = 'html_namespacing_ext'
4
+
5
+ dir_config(name)
6
+ create_makefile(name)
@@ -0,0 +1,531 @@
1
+ #include <assert.h>
2
+ #include <errno.h>
3
+ #include <stdlib.h>
4
+ #include <string.h>
5
+
6
+ #include <stdio.h>
7
+
8
+ #include "html_namespacing.h"
9
+
10
+ static const char* const IGNORE_TAGS[] = {
11
+ "html",
12
+ "head",
13
+ "base",
14
+ "meta",
15
+ "title",
16
+ "link",
17
+ "script",
18
+ "noscript",
19
+ "style",
20
+ NULL
21
+ };
22
+
23
+ /*
24
+ * Returns the first power of 2 which is >= the given length.
25
+ */
26
+ static int
27
+ round_to_power_of_two(int i)
28
+ {
29
+ int j = 1;
30
+
31
+ while (j < i) {
32
+ j <<= 1;
33
+ }
34
+
35
+ return j;
36
+ }
37
+
38
+ /*
39
+ * Returns the number of bytes in the first utf-8 character of *utf8.
40
+ */
41
+ static size_t
42
+ utf8_char_bytes(const char *utf8)
43
+ {
44
+ size_t i;
45
+ const unsigned char *u;
46
+
47
+ if (utf8[0] < 0x80) {
48
+ return 1;
49
+ }
50
+
51
+ u = (const unsigned char *) utf8;
52
+
53
+ for (i = 1; i < 6; i++) {
54
+ if ((u[i] & 0xc0) != 0x80) {
55
+ return i;
56
+ }
57
+ }
58
+
59
+ return -1;
60
+ }
61
+
62
+ /*
63
+ * Ensures *r is long enough to hold len chars (using realloc).
64
+ *
65
+ * Arguments:
66
+ * - char **r: Pointer to the string (which need not be \0-terminated). Will
67
+ * be replaced by the new string.
68
+ * - size_t *r_len: Pointer to the length of the string. Will be replaced by
69
+ * the new length, which will be >= len.
70
+ * - size_t len: Minimum length of the new string.
71
+ *
72
+ * Returns:
73
+ * - 0 on success, whether or not *r changed length.
74
+ * - ENOMEM if reallocation failed. *r and *r_len will point to their old
75
+ * values, which will remain unchanged.
76
+ */
77
+ static int
78
+ ensure_string_length(
79
+ char **r,
80
+ size_t *r_len,
81
+ size_t len,
82
+ HtmlNamespacingAllocationStrategy allocation_strategy)
83
+ {
84
+ char *new_r;
85
+ size_t new_r_len;
86
+
87
+ new_r_len = *r_len;
88
+ if (new_r_len >= len) {
89
+ /* No need to realloc */
90
+ return 0;
91
+ }
92
+
93
+ while (new_r_len < len) {
94
+ new_r_len <<= 1;
95
+ }
96
+
97
+ new_r = allocation_strategy.realloc(*r, sizeof(char) * new_r_len);
98
+ if (!new_r) {
99
+ return ENOMEM;
100
+ }
101
+
102
+ *r = new_r;
103
+ *r_len = new_r_len;
104
+ return 0;
105
+ }
106
+
107
+ /*
108
+ * Tries to copy a single utf8 character to dest, possibly reallocating.
109
+ *
110
+ * Arguments:
111
+ * - dest: Beginning of destination string. May be reallocated during copy.
112
+ * - dest_len: Amount of memory allocated to dest. May be increased during
113
+ * copy.
114
+ * - dest_p: Pointer to end of destination string (potentially 1 past the
115
+ * allocated length of dest).
116
+ * - src_p: Source string. Will be advanced.
117
+ *
118
+ * Returns:
119
+ * - 0 on success. dest may be changed; dest_p will be incremented; src_p
120
+ * will be incremented.
121
+ * - ENOMEM if reallocation failed. dest, dest_p, and src_p will remain
122
+ * unchanged.
123
+ */
124
+ static int
125
+ append_next_utf8_char(
126
+ char **dest,
127
+ size_t *dest_len,
128
+ char **dest_p,
129
+ const char **src_p,
130
+ HtmlNamespacingAllocationStrategy allocation_strategy)
131
+ {
132
+ size_t c_num_utf8_bytes;
133
+ size_t dest_p_offset;
134
+ size_t new_dest_len;
135
+ char *new_dest;
136
+ int rv;
137
+
138
+ c_num_utf8_bytes = utf8_char_bytes(*src_p);
139
+ dest_p_offset = *dest_p - *dest;
140
+ new_dest = *dest;
141
+ new_dest_len = dest_p_offset + c_num_utf8_bytes;
142
+
143
+ rv = ensure_string_length(
144
+ &new_dest, dest_len, new_dest_len, allocation_strategy);
145
+ if (rv == ENOMEM) {
146
+ return ENOMEM;
147
+ }
148
+ if (new_dest != *dest) {
149
+ *dest = new_dest;
150
+ *dest_p = new_dest + dest_p_offset;
151
+ }
152
+
153
+ strncpy(*dest_p, *src_p, c_num_utf8_bytes);
154
+ *dest_p += c_num_utf8_bytes;
155
+ *src_p += c_num_utf8_bytes;
156
+
157
+ return 0;
158
+ }
159
+
160
+ static int
161
+ append_string(
162
+ char **dest,
163
+ size_t *dest_len,
164
+ char **dest_p,
165
+ const char *s,
166
+ HtmlNamespacingAllocationStrategy allocation_strategy)
167
+ {
168
+ int rv = 0;
169
+
170
+ while (*s && rv == 0) {
171
+ rv = append_next_utf8_char(
172
+ dest, dest_len, dest_p, &s, allocation_strategy);
173
+ }
174
+
175
+ return rv;
176
+ };
177
+
178
+ static int
179
+ append_end_of_string(
180
+ char **dest,
181
+ size_t *dest_len,
182
+ char **dest_p,
183
+ HtmlNamespacingAllocationStrategy allocation_strategy)
184
+ {
185
+ int rv;
186
+ const char *end = "\0";
187
+
188
+ rv = append_next_utf8_char(
189
+ dest, dest_len, dest_p, &end, allocation_strategy);
190
+
191
+ if (rv == 0) {
192
+ /* We don't add the length of '\0' */
193
+ *dest_len -= 1;
194
+ *dest_p -= 1;
195
+ }
196
+
197
+ return rv;
198
+ }
199
+
200
+ enum {
201
+ PARSE_NORMAL,
202
+ PARSE_OPEN_TAG,
203
+ PARSE_OPEN_TAG_NAME,
204
+ PARSE_OPEN_TAG_ATTRIBUTE_NAME,
205
+ PARSE_OPEN_TAG_ATTRIBUTE_VALUE,
206
+ PARSE_OPEN_TAG_ATTRIBUTE_EQUALS,
207
+ PARSE_CLOSE_TAG,
208
+ PARSE_EMPTY_TAG, /* detected while we're inside PARSE_OPEN_TAG */
209
+ PARSE_COMMENT,
210
+ PARSE_XML_DECL,
211
+ PARSE_DOCTYPE,
212
+ PARSE_CDATA
213
+ };
214
+
215
+ static int
216
+ char_is_whitespace(char c)
217
+ {
218
+ return c == 0x20 || c == 0x09 || c == 0x0d || c == 0x0a;
219
+ }
220
+
221
+ static int
222
+ should_ignore_tag(const char *tag_name, size_t tag_len)
223
+ {
224
+ int i = 0;
225
+ const char *test_ignore;
226
+
227
+ for (i = 0; test_ignore = IGNORE_TAGS[i]; i++) {
228
+ if (0 == strncmp(test_ignore, tag_name, tag_len)
229
+ && strlen(test_ignore) == tag_len)
230
+ {
231
+ return 1;
232
+ }
233
+ }
234
+
235
+ return 0;
236
+ }
237
+
238
+ /*
239
+ * Adjusts the given HTML so that top-level elements get an added "class" of
240
+ * namespace.
241
+ *
242
+ * For instance, given
243
+ * "<div><span>Hello</span></div><p class="below">Goodbye</p>" and a
244
+ * namespace of "foo", returns
245
+ * "<div class="foo"><span>Hello</span></div><p class="below foo">Goodbye</p>".
246
+ *
247
+ * Arguments:
248
+ * - const char *html: Original HTML string. Must be valid HTML, encoded as
249
+ * utf-8.
250
+ * - int html_len: Length of HTML string.
251
+ * - const char *namespace: Namespace to add. Must be of the form
252
+ * "[a-zA-Z][-_a-zA-Z0-9]*".
253
+ * - char **ret: Pointer to return value, which will be newly-allocated. Will
254
+ * be overwritten only on success.
255
+ * - int *ret_len: Pointer to return value's length. Will be overwritten only
256
+ * on success.
257
+ *
258
+ * Returns:
259
+ * - 0 on success. *ret will be overwritten to point to the newly-allocated
260
+ * string.
261
+ * - ENOMEM if *ret could not be allocated.
262
+ * - EINVAL if html is not valid XML.
263
+ */
264
+ int
265
+ add_namespace_to_html_with_length_and_allocation_strategy(
266
+ const char *html,
267
+ size_t html_len,
268
+ const char *ns,
269
+ char **ret,
270
+ size_t *ret_len,
271
+ HtmlNamespacingAllocationStrategy allocation_strategy)
272
+ {
273
+
274
+ #define APPEND_NEXT_CHAR() \
275
+ if (append_next_utf8_char(&r, &r_len, &r_p, &html, allocation_strategy) != 0) goto error/*;*/
276
+ #define APPEND_STRING(s) \
277
+ if (append_string(&r, &r_len, &r_p, s, allocation_strategy) != 0) goto error/*;*/
278
+ #define APPEND_END_OF_STRING() \
279
+ if (append_end_of_string(&r, &r_len, &r_p, allocation_strategy) != 0) goto error/*;*/
280
+
281
+ unsigned int state;
282
+ char *r; /* Start of retval */
283
+ char *r_p; /* Pointer in retval */
284
+ size_t r_len; /* Length of retval */
285
+ const char *html_start;
286
+ const char *open_tag_name;
287
+ size_t open_tag_name_len;
288
+ char c;
289
+ size_t num_chars_remaining;
290
+ int num_tags_open;
291
+ int open_tag_attribute_is_class_attribute;
292
+ int open_tag_had_class_attribute;
293
+ const char *open_tag_attribute_value;
294
+
295
+ r_len = round_to_power_of_two(html_len);
296
+ r = allocation_strategy.malloc(sizeof(char) * r_len);
297
+ if (!r) {
298
+ return ENOMEM;
299
+ }
300
+
301
+ html_start = html;
302
+ state = PARSE_NORMAL;
303
+ r_p = r;
304
+ num_tags_open = 0;
305
+ open_tag_attribute_is_class_attribute = 0;
306
+ open_tag_had_class_attribute = 0;
307
+ open_tag_attribute_value = NULL;
308
+
309
+ while (1) {
310
+ num_chars_remaining = html_len - (html - html_start);
311
+ if (num_chars_remaining <= 0) break;
312
+
313
+ c = *html;
314
+ switch (state) {
315
+ case PARSE_NORMAL:
316
+ if (c == '<') {
317
+ APPEND_NEXT_CHAR();
318
+ if (num_chars_remaining >= 9
319
+ && 0 == strncmp("![CDATA[", html, 8)) {
320
+ state = PARSE_CDATA;
321
+ } else if (num_chars_remaining >= 2 && html[0] == '/') {
322
+ state = PARSE_CLOSE_TAG;
323
+ } else if (num_chars_remaining >= 4
324
+ && 0 == strncmp("!--", html, 3)) {
325
+ state = PARSE_COMMENT;
326
+ } else if (num_chars_remaining >= 9
327
+ && 0 == strncmp("!DOCTYPE", html, 8)) {
328
+ state = PARSE_DOCTYPE;
329
+ } else if (num_chars_remaining >= 5
330
+ && 0 == strncmp("?xml", html, 4)) {
331
+ state = PARSE_XML_DECL;
332
+ } else {
333
+ open_tag_name = html;
334
+ state = PARSE_OPEN_TAG_NAME;
335
+ }
336
+ } else {
337
+ APPEND_NEXT_CHAR();
338
+ }
339
+ break;
340
+ case PARSE_OPEN_TAG_NAME:
341
+ if (char_is_whitespace(c) || c == '>' || c == '/') {
342
+ open_tag_name_len = html - open_tag_name;
343
+ state = PARSE_OPEN_TAG;
344
+ }
345
+ if (c != '>' && c != '/') {
346
+ APPEND_NEXT_CHAR();
347
+ }
348
+ break;
349
+ case PARSE_OPEN_TAG:
350
+ if (c == '/' || c == '>') {
351
+ if (num_tags_open == 0 && !open_tag_had_class_attribute
352
+ && !should_ignore_tag(open_tag_name, open_tag_name_len)) {
353
+ APPEND_STRING(" class=\"");
354
+ APPEND_STRING(ns);
355
+ APPEND_STRING("\"");
356
+ }
357
+
358
+ open_tag_had_class_attribute = 0;
359
+ open_tag_attribute_value = NULL;
360
+
361
+ if (c == '/') {
362
+ APPEND_STRING(" ");
363
+ state = PARSE_EMPTY_TAG;
364
+ } else {
365
+ num_tags_open++;
366
+ state = PARSE_NORMAL;
367
+ }
368
+ } else if (!char_is_whitespace(c)) {
369
+ if (num_chars_remaining >= 5
370
+ && 0 == strncmp(html, "class", 5)) {
371
+ open_tag_attribute_is_class_attribute = 1;
372
+ open_tag_had_class_attribute = 1;
373
+ } else {
374
+ open_tag_attribute_is_class_attribute = 0;
375
+ }
376
+ state = PARSE_OPEN_TAG_ATTRIBUTE_NAME;
377
+ }
378
+ APPEND_NEXT_CHAR();
379
+ break;
380
+ case PARSE_OPEN_TAG_ATTRIBUTE_NAME:
381
+ if (char_is_whitespace(c) || c == '=') {
382
+ state = PARSE_OPEN_TAG_ATTRIBUTE_EQUALS;
383
+ }
384
+ APPEND_NEXT_CHAR();
385
+ break;
386
+ case PARSE_OPEN_TAG_ATTRIBUTE_EQUALS:
387
+ if (c == '\'' || c == '"') {
388
+ open_tag_attribute_value = html;
389
+ state = PARSE_OPEN_TAG_ATTRIBUTE_VALUE;
390
+ }
391
+ APPEND_NEXT_CHAR();
392
+ break;
393
+ case PARSE_OPEN_TAG_ATTRIBUTE_VALUE:
394
+ /* open_tag_attribute_value is either ' or " */
395
+ if (c == *open_tag_attribute_value) {
396
+ if (open_tag_attribute_is_class_attribute
397
+ && num_tags_open == 0) {
398
+ APPEND_STRING(" ");
399
+ APPEND_STRING(ns);
400
+ }
401
+ open_tag_attribute_is_class_attribute = 0;
402
+ state = PARSE_OPEN_TAG;
403
+ }
404
+ APPEND_NEXT_CHAR();
405
+ break;
406
+ case PARSE_CLOSE_TAG:
407
+ if (c == '>') {
408
+ num_tags_open--;
409
+ open_tag_attribute_value = NULL;
410
+ state = PARSE_NORMAL;
411
+ }
412
+ APPEND_NEXT_CHAR();
413
+ break;
414
+ case PARSE_EMPTY_TAG:
415
+ case PARSE_XML_DECL:
416
+ case PARSE_DOCTYPE:
417
+ if (c == '>') {
418
+ state = PARSE_NORMAL;
419
+ }
420
+ APPEND_NEXT_CHAR();
421
+ break;
422
+ case PARSE_COMMENT:
423
+ APPEND_NEXT_CHAR();
424
+ if (c == '-' && num_chars_remaining >= 3
425
+ && 0 == strncmp("->", html, 2)) {
426
+ APPEND_NEXT_CHAR();
427
+ APPEND_NEXT_CHAR();
428
+ state = PARSE_NORMAL;
429
+ }
430
+ break;
431
+ case PARSE_CDATA:
432
+ APPEND_NEXT_CHAR();
433
+ if (c == ']' && num_chars_remaining >= 3
434
+ && 0 == strncmp("]>", html, 2)) {
435
+ APPEND_NEXT_CHAR();
436
+ APPEND_NEXT_CHAR();
437
+ state = PARSE_NORMAL;
438
+ }
439
+ break;
440
+ default:
441
+ assert(0);
442
+ }
443
+ }
444
+
445
+ APPEND_END_OF_STRING();
446
+
447
+ if (state != PARSE_NORMAL
448
+ || (r_p - r) < html_len
449
+ || num_chars_remaining != 0
450
+ || num_tags_open != 0
451
+ || open_tag_attribute_is_class_attribute
452
+ || open_tag_attribute_value)
453
+ {
454
+ allocation_strategy.free (r);
455
+ *ret = NULL;
456
+ *ret_len = -1;
457
+ return EINVAL;
458
+ }
459
+
460
+ *ret = r;
461
+ *ret_len = r_p - r;
462
+ return 0;
463
+
464
+ error:
465
+ allocation_strategy.free(r);
466
+ *ret = NULL;
467
+ *ret_len = -1;
468
+ return ENOMEM;
469
+ }
470
+
471
+ int
472
+ add_namespace_to_html_with_length(
473
+ const char *html,
474
+ size_t html_len,
475
+ const char *ns,
476
+ char **ret,
477
+ size_t *ret_len)
478
+ {
479
+ HtmlNamespacingAllocationStrategy allocation_strategy;
480
+ allocation_strategy.malloc = malloc;
481
+ allocation_strategy.free = free;
482
+ allocation_strategy.realloc = realloc;
483
+
484
+ return add_namespace_to_html_with_length_and_allocation_strategy(
485
+ html, html_len, ns, ret, ret_len, allocation_strategy);
486
+ }
487
+
488
+
489
+ #if INCLUDE_MAIN
490
+ #include <stdio.h>
491
+
492
+ int
493
+ main(int argc, char **argv)
494
+ {
495
+ const char *html;
496
+ size_t html_len;
497
+ const char *ns;
498
+ char *ret;
499
+ size_t ret_len;
500
+ int rv;
501
+ HtmlNamespacingAllocationStrategy allocation_strategy;
502
+
503
+ if (argc != 3) {
504
+ printf("usage: %s HTML_STRING NAMESPACE", argv[0]);
505
+ return 1;
506
+ }
507
+
508
+ html = argv[1];
509
+ html_len = strlen(html);
510
+ ns = argv[2];
511
+
512
+ printf("html: %s\n", html);
513
+ printf("html_len: %d\n", html_len);
514
+ printf("ns: %s\n", ns);
515
+
516
+ allocation_strategy.malloc = malloc;
517
+ allocation_strategy.free = free;
518
+ allocation_strategy.realloc = realloc;
519
+
520
+ rv = add_namespace_to_html_with_length_and_allocation_strategy(html, html_len, ns, &ret, &ret_len, allocation_strategy);
521
+
522
+ if (ret) {
523
+ printf("ret: %s\n", ret);
524
+ printf("ret_len: %d\n", ret_len);
525
+ } else {
526
+ printf("Failed!\n");
527
+ }
528
+
529
+ return rv;
530
+ }
531
+ #endif /* INCLUDE_MAIN */
@@ -0,0 +1,35 @@
1
+ #ifndef HTML_NAMESPACING_H
2
+ #define HTML_NAMESPACING_H
3
+
4
+ #ifdef __cplusplus
5
+ extern "C" {
6
+ #endif
7
+
8
+ typedef struct _HtmlNamespacingAllocationStrategy {
9
+ void *(*malloc)(size_t size);
10
+ void (*free)(void *ptr);
11
+ void *(*realloc)(void *ptr, size_t size);
12
+ } HtmlNamespacingAllocationStrategy;
13
+
14
+ int
15
+ add_namespace_to_html_with_length(
16
+ const char *html,
17
+ size_t html_len,
18
+ const char *ns,
19
+ char **ret,
20
+ size_t *ret_len);
21
+
22
+ int
23
+ add_namespace_to_html_with_length_and_allocation_strategy(
24
+ const char *html,
25
+ size_t html_len,
26
+ const char *ns,
27
+ char **ret,
28
+ size_t *ret_len,
29
+ HtmlNamespacingAllocationStrategy allocation_strategy);
30
+
31
+ #ifdef __cplusplus
32
+ }
33
+ #endif
34
+
35
+ #endif /* HTML_NAMESPACING_H */
@@ -0,0 +1,97 @@
1
+ #include <errno.h>
2
+
3
+ #include "ruby.h"
4
+
5
+ #include "html_namespacing.h"
6
+
7
+ #include <stdio.h>
8
+
9
+ VALUE rb_mHtmlNamespacing = Qnil;
10
+
11
+ static const HtmlNamespacingAllocationStrategy ALLOCATION_STRATEGY = {
12
+ (void *(*)(size_t)) ruby_xmalloc,
13
+ ruby_xfree,
14
+ (void *(*)(void *, size_t)) ruby_xrealloc
15
+ };
16
+
17
+ /*
18
+ * call-seq:
19
+ * HtmlNamespacing.add_namespace_to_html(html, ns) => String
20
+ *
21
+ * Returns new HTML string based on +html+ with the HTML class +ns+ to root
22
+ * elements.
23
+ *
24
+ * If +html+ is nil, returns +nil+.
25
+ *
26
+ * If +ns+ is nil, returns +html+.
27
+ */
28
+
29
+ VALUE
30
+ html_namespacing_add_namespace_to_html(
31
+ VALUE obj,
32
+ VALUE html,
33
+ VALUE ns)
34
+ {
35
+ /*
36
+ * It's almost tempting to manually allocate the RString object to save
37
+ * ourselves the stupid extra copy. (add_namespace_to_html_with_length()
38
+ * implicitly copies the string, and here we are copying it again because
39
+ * Ruby can't convert from a char* to an RString? How lame....)
40
+ *
41
+ * But for now, let's just do the extra copy (implicit in rb_str_new2) and
42
+ * be done with it.
43
+ */
44
+
45
+ const char *html_ptr;
46
+ size_t html_len;
47
+ const char *ns_ptr;
48
+ char *ret_ptr;
49
+ size_t ret_len;
50
+ int rv;
51
+ VALUE ret;
52
+
53
+ if (TYPE(html) == T_NIL) {
54
+ return Qnil;
55
+ }
56
+ Check_Type(html, T_STRING);
57
+
58
+ if (TYPE(ns) == T_NIL) {
59
+ return html;
60
+ }
61
+
62
+ Check_Type(ns, T_STRING);
63
+
64
+ html_ptr = RSTRING(html)->ptr;
65
+ html_len = RSTRING(html)->len;
66
+
67
+ ns_ptr = RSTRING(ns)->ptr;
68
+
69
+ rv = add_namespace_to_html_with_length_and_allocation_strategy(
70
+ html_ptr,
71
+ html_len,
72
+ ns_ptr,
73
+ &ret_ptr,
74
+ &ret_len,
75
+ ALLOCATION_STRATEGY);
76
+ if (rv == EINVAL) {
77
+ rb_raise(rb_eArgError, "Badly-formed HTML");
78
+ }
79
+ if (rv != 0) {
80
+ rb_raise(rb_eRuntimeError, "Unknown error in add_namespace_to_html");
81
+ }
82
+
83
+ ret = rb_str_new(ret_ptr, ret_len);
84
+ ruby_xfree(ret_ptr);
85
+
86
+ return ret;
87
+ }
88
+
89
+ /*
90
+ * Holds functions related to HTML namespacing.
91
+ */
92
+ void
93
+ Init_html_namespacing_ext()
94
+ {
95
+ rb_mHtmlNamespacing = rb_define_module("HtmlNamespacing");
96
+ rb_define_module_function(rb_mHtmlNamespacing, "add_namespace_to_html", html_namespacing_add_namespace_to_html, 2);
97
+ }
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{html_namespacing}
5
+ s.version = "0.0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Adam Hooper"]
9
+ s.date = %q{2009-03-17}
10
+ s.description = %q{Inserts "class=" attributes within snippets of HTML so CSS and JavaScript can use automatic scopes}
11
+ s.email = %q{adam@adamhooper.com}
12
+ s.extensions = ["ext/html_namespacing/extconf.rb"]
13
+ s.extra_rdoc_files = ["README.rdoc"]
14
+ s.files = ["README.rdoc", "Rakefile", "test/c_extension_test.rb", "test/test_helper.rb", "ext/html_namespacing/extconf.rb", "ext/html_namespacing/html_namespacing.h", "ext/html_namespacing/html_namespacing.c", "ext/html_namespacing/html_namespacing_ext.c", "lib/html_namespacing.rb", "lib/html_namespacing/plugin/rails.rb", "Manifest", "html_namespacing.gemspec"]
15
+ s.has_rdoc = true
16
+ s.homepage = %q{http://github.com/adamh/html_namespacing}
17
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Html_namespacing", "--main", "README.rdoc"]
18
+ s.require_paths = ["lib", "ext"]
19
+ s.rubyforge_project = %q{html_namespacing}
20
+ s.rubygems_version = %q{1.3.1}
21
+ s.summary = %q{Automatic HTML namespacing}
22
+ s.test_files = ["test/c_extension_test.rb", "test/test_helper.rb"]
23
+
24
+ if s.respond_to? :specification_version then
25
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
26
+ s.specification_version = 2
27
+
28
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
29
+ s.add_development_dependency(%q<echoe>, [">= 0"])
30
+ else
31
+ s.add_dependency(%q<echoe>, [">= 0"])
32
+ end
33
+ else
34
+ s.add_dependency(%q<echoe>, [">= 0"])
35
+ end
36
+ end
@@ -0,0 +1,6 @@
1
+ require File.dirname(__FILE__) + '/../ext/html_namespacing/html_namespacing_ext'
2
+
3
+ require 'html_namespacing/plugin/rails'
4
+
5
+ module HtmlNamespacing
6
+ end
@@ -0,0 +1,42 @@
1
+ module HtmlNamespacing
2
+ module Plugin
3
+ module Rails
4
+ def self.install(options = {})
5
+ @options = options
6
+ install_rails_2_3
7
+ end
8
+
9
+ # Called by ActionView
10
+ def self.path_to_namespace(template)
11
+ if func = @options[:template_to_namespace_callback]
12
+ func.call(template)
13
+ elsif template.path =~ /^([^\.]+)/
14
+ $1.gsub('/', '-')
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def self.install_rails_2_3(options = {})
21
+ ::ActionView::Template.class_eval do
22
+ def render_template_with_html_namespacing(*args)
23
+ s = render_template_without_html_namespacing(*args)
24
+
25
+ if format == 'html'
26
+ HtmlNamespacing::add_namespace_to_html(s, html_namespace)
27
+ else
28
+ s
29
+ end
30
+ end
31
+
32
+ alias_method_chain :render_template, :html_namespacing
33
+
34
+ def html_namespace
35
+ HtmlNamespacing::Plugin::Rails.path_to_namespace(self)
36
+ end
37
+ memoize :html_namespace
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class CExtensionTest < Test::Unit::TestCase
6
+ private
7
+
8
+ def self.define_test(name, html, ns, expect)
9
+ define_method("test_#{name}".to_sym) do
10
+ assert_equal(expect, f(html, ns))
11
+ end
12
+ end
13
+
14
+ def self.define_failing_test(name, html)
15
+ define_method("test_#{name}".to_sym) do
16
+ assert_raises(ArgumentError) do
17
+ f(html, 'X')
18
+ end
19
+ end
20
+ end
21
+
22
+ self.define_test('nil HTML', nil, 'X', nil)
23
+ self.define_test('nil namespace', '<div>hello</div>', nil, '<div>hello</div>')
24
+ self.define_test('nil everything', nil, nil, nil)
25
+ self.define_test('plain text', 'hello', 'X', 'hello')
26
+ self.define_test('regular tag', '<div>hello</div>', 'X', '<div class="X">hello</div>')
27
+ self.define_test('empty tag', '<div/>', 'X', '<div class="X" />')
28
+ self.define_test('nested tag', '<div><div>hello</div></div>', 'X', '<div class="X"><div>hello</div></div>')
29
+ self.define_test('two tags', '<div>hello</div><div>goodbye</div>', 'X', '<div class="X">hello</div><div class="X">goodbye</div>')
30
+ self.define_test('existing class= tag with double-quotes', '<div class="foo">bar</div>', 'baz', '<div class="foo baz">bar</div>')
31
+ self.define_test('existing class= tag with single-quotes', "<div class='foo'>bar</div>", 'baz', "<div class='foo baz'>bar</div>")
32
+ self.define_test('other attributes are ignored', '<div id="id" class="foo" style="display:none;">bar</div>', 'baz', '<div id="id" class="foo baz" style="display:none;">bar</div>')
33
+ self.define_test('works with utf-8', '<div class="𝞪">𝟂</div>', '𝞺', '<div class="𝞪 𝞺">𝟂</div>')
34
+ self.define_test('empty tag with existing class=', '<span class="foo"/>', 'bar', '<span class="foo bar" />')
35
+ self.define_test('works with newlines in tag', "<div\n\nclass\n\n=\n\n'foo'\n\n>bar</div>", 'baz', "<div\n\nclass\n\n=\n\n'foo baz'\n\n>bar</div>")
36
+ self.define_test('ignores XML prolog', '<?xml version="1.0"?><div>foo</div>', 'X', '<?xml version="1.0"?><div class="X">foo</div>')
37
+ self.define_test('ignores DOCTYPE', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><div>foo</div>', 'X', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><div class="X">foo</div>')
38
+ self.define_test('ignores CDATA', '<![CDATA[ignore <div class="foo">]] </div>]]><div>foo</div>', 'X', '<![CDATA[ignore <div class="foo">]] </div>]]><div class="X">foo</div>')
39
+ self.define_test('ignores comments', '<!-- blah <div class="foo">foo</div> - <span/>--><div>foo</div>', 'X', '<!-- blah <div class="foo">foo</div> - <span/>--><div class="X">foo</div>')
40
+
41
+ [ 'html', 'head', 'base', 'meta', 'title', 'link', 'script', 'noscript', 'style' ].each do |tag|
42
+ self.define_test("ignores <#{tag}> tags", "<#{tag}>foo</#{tag}>", "X", "<#{tag}>foo</#{tag}>")
43
+ end
44
+
45
+ self.define_failing_test('unclosed tag fails', '<div>foo')
46
+ self.define_failing_test('closing tag fails', 'foo</div>')
47
+ self.define_failing_test('wrong attr syntax fails', '<div foo=bar>foo</div>')
48
+
49
+ def f(html, ns)
50
+ HtmlNamespacing::add_namespace_to_html(html, ns)
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ require 'test/unit'
2
+
3
+ require File.dirname(__FILE__) + '/../lib/html_namespacing'
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adamh-html_namespacing
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Adam Hooper
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-17 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: echoe
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: Inserts "class=" attributes within snippets of HTML so CSS and JavaScript can use automatic scopes
26
+ email: adam@adamhooper.com
27
+ executables: []
28
+
29
+ extensions:
30
+ - ext/html_namespacing/extconf.rb
31
+ extra_rdoc_files:
32
+ - README.rdoc
33
+ files:
34
+ - README.rdoc
35
+ - Rakefile
36
+ - test/c_extension_test.rb
37
+ - test/test_helper.rb
38
+ - ext/html_namespacing/extconf.rb
39
+ - ext/html_namespacing/html_namespacing.h
40
+ - ext/html_namespacing/html_namespacing.c
41
+ - ext/html_namespacing/html_namespacing_ext.c
42
+ - lib/html_namespacing.rb
43
+ - lib/html_namespacing/plugin/rails.rb
44
+ - Manifest
45
+ - html_namespacing.gemspec
46
+ has_rdoc: true
47
+ homepage: http://github.com/adamh/html_namespacing
48
+ post_install_message:
49
+ rdoc_options:
50
+ - --line-numbers
51
+ - --inline-source
52
+ - --title
53
+ - Html_namespacing
54
+ - --main
55
+ - README.rdoc
56
+ require_paths:
57
+ - lib
58
+ - ext
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "1.2"
70
+ version:
71
+ requirements: []
72
+
73
+ rubyforge_project: html_namespacing
74
+ rubygems_version: 1.2.0
75
+ signing_key:
76
+ specification_version: 2
77
+ summary: Automatic HTML namespacing
78
+ test_files:
79
+ - test/c_extension_test.rb
80
+ - test/test_helper.rb