skeeto / skewer-mode

Live web development in Emacs
The Unlicense
1.1k stars 57 forks source link

Skewer-html mode (like skewer-css) #19

Closed ZaneA closed 11 years ago

ZaneA commented 11 years ago

Hi there,

I have written a skewer-html mode using skewer-css as a base. The additional JavaScript hasn't yet been tested on IE (which I guess will need a quick fix for the innerHTML), but otherwise this is working great for me.

It adds a few functions that allow you to "eval" HTML (skewer-html-eval, and interactive helpers), which in this case translates to appending HTML to the body, but using the prefix argument you can easily specify an alternative selector and choose to replace instead of append. There is also another function that lets you pull HTML from a selector into the current buffer (skewer-html-fetch-selector), which I find useful if I have been appending stuff for a while.

I have also updated the README to reflect the new keybindings.

Let me know if anything seems a bit off with this patch, but hopefully all is good and can be merged as is.

Cheers, Zane

skeeto commented 11 years ago

This is really interesting idea. I've been playing around with it to get the hang of it. Have you found this to be a productive workflow yet? If so, could you describe it?

I'm not convinced on the current semantics of the commands. Appending the closest-wrapping tags to body doesn't seem like a useful default. On the other hand, replacing/updating the corresponding contents on the page, without prompting for a selector, does seem useful. For example, in skewer-css, even though mechanically the rule is being appended to the current set of rules, the new rule overrides them, replacing/updating parts of the old rules.

Perhaps skewer-html-eval-tag should compute a selector based on the tag's structural position (i.e. body > article > p:nth-child(3)) and use that to update the content at that position on the page, inserting it if it doesn't yet exist. You're already halfway there, except that you prompt the user to write a selector.

I've taken your commits into the skewer-html branch in my repository, making just a few stylistic changes. I like this idea but I want to refine it further before merging it into master.

ZaneA commented 11 years ago

Thanks for the feedback skeeto!

I do find this to be a productive workflow, but it does depend on the situation.

The way I have been using it is to build up pages in a bottom-up way using this mode, as opposed to editing an existing one. I start by by adding an element or two to the body, and then writing JavaScript to act on those elements, and then gradually either appending more elements to the body, or into those existing elements, writing further JavaScript etc. Once I have something resembling a useful DOM, I use skewer-html-fetch-selector-into-buffer to fetch the entire body into the buffer which is then saved into a file for reuse.

The other situation where I find appending by default to be useful is when you inject skewer into an existing website. I can then write some HTML and append it into an existing element. Whereas a default of replacing the body wouldn't be useful in this situation (although replacing a specific selector could be).

So really it does just depend on the workflow you have, at the end of the day I have no objection to replacing by default if you feel that it will suit the general skewer workflow better (and I agree that it probably does for most people).

For skewer-html-eval-tag I actually tried to implement a form of computing initially, but with the way I work my buffer doesn't always contain the full page so in my case computing a selector like this won't work. What I did try though was to find the parent of the current tag, and look for an ID attribute that could be used for replacing (if even just as a prompt default) but this was also a limited case. I'm open to suggestions though and would really like to see the selector computing functionality in there, but perhaps as a separate function (or as another option).

On another note, you may have noticed that when you DO replace the body element using skewer-html, you will lose any CSS customizations that you have made (I guess the only way around this would be to move these to the head instead).

So where do we go from here? :)

skeeto commented 11 years ago

Check out my skewer-html branch now. skewer-html-eval-tag now computes a selector for the current tag (skewer-html-compute-selector) and skewer.fn.html replaces the selected tag with the provided HTML. I wanted to go beyond innerHTML so that the update would include any changes in attributes as well. It's not in there yet, but I'm thinking the prefix would tell skewer to append the evaluated tag to the current parent rather than replace the tag at that position.

I still need to work out appending when the selected tag doesn't currently exist. I figure this would work by passing the ancestry chain as an array along with the selector. If the selector returns nothing, Skewer would use the ancestry list to figure out at what point the structure does already exist, then create a bunch of empty containers for the rest building the ancestry and siblings, then finally inserting the new tag.

I removed eval-region for now since it doesn't entirely make sense with with this new selector behavior. Something like it may be worth putting back in.

I like your fetching idea that you already have, and I imagine a workflow could be like this: bring up an empty/partial HTML buffer. Fetch the current DOM into it, then manipulate it as text and "evaluate" tags to update the DOM on the other side.

Except for perhaps this fetching, I really want to avoid asking the user to type out a selector. The minor mode should be as smart about that as possible.

ZaneA commented 11 years ago

Awesome work! This is certainly a much nicer approach than I could've imagined and even better, will work for both of those main use cases.

Re-implementing eval-region shouldn't be too difficult once eval-tag is complete, I made a rough first pass of it by finding each element within the active region, and then simply running eval-tag on each. Something like this (as always I'm sure there's a cleaner approach!)

(defun skewer-html-eval-region (beg end &optional prefix)
  "Load HTML from region. When prefixed, append."
  (interactive "r\nP")
  (save-excursion
    (goto-char beg)
    (while (< (point) end)
      (re-search-forward "[ \t\r\n]*?" nil t)
      (forward-char)
      (skewer-html-eval-tag prefix)
      (sgml-skip-tag-forward 2)
      (when (<= (point) end)
        (sgml-skip-tag-backward 1)))
    ))

I appreciate you running with this, and really look forward to seeing it in master :)

skeeto commented 11 years ago

I just implemented new tag insertion. A selector is no longer sent to skewer, just the ancestry list that specifies the path. Internally it's still using the handy querySelector, though. What's new is if an element cannot be found, the path to that element is constructed out of empty elements, then the content is inserted in place.

Unfortunately, your new eval-region function is still redundant here. Evaluating the top-level element in the region will automatically capture everything below it -- even if it's not in the region.

With this last change, I think I'm ready to merge it into master. It's still a bit flaky in some special cases, like evaling body directly, but it's pretty usable for the most part. I'll fix a few of these just before merging, or at least have it fail more gracefully. Do you have any input before I do the merge?

skeeto commented 11 years ago

On second thought, if there are multiple top-level siblings in the region, your eval-region would be useful. I'll work that in, too.

ZaneA commented 11 years ago

Sounds great skeeto, please do merge :)

skeeto commented 11 years ago

Touched up and merged.

flashingpumpkin commented 11 years ago

Hi. Just a quick question: How would one load additional JavaScript dependencies when calling skewer-html-eval-tag?

The following example does not load the angular.min.js file when doing C-M-x:

    <div>
      <script type="text/javascript" src="http://code.angularjs.org/1.0.6/angular.min.js"></script>
    </div>
skeeto commented 11 years ago

@flashingpumpkin You're seeing some of the rough edges that I never fully worked out. The problem is that skewer-html is expecting the buffer to house an entire HTML document all the way up to the html tag, so that it can do the structure matching.

<html>
  <body>
    <div>
      <script type="text/javascript" src="http://code.angularjs.org/1.0.6/angular.min.js"></script>
    </div>
  </body>
</html>

It should work if your buffer looks like the above.