adamh-html_namespacing 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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