ramnathv / slidify

Generate reproducible html5 slides from R markdown
http://www.slidify.org
844 stars 339 forks source link

Request: Custom rendering with css selectors #179

Open jamiefolson opened 11 years ago

jamiefolson commented 11 years ago

I mentioned that it would be nice to be able to do something like knitr's hooks. Between blocks and layouts and partials, there are a lot of options. However, some changes may be easier to accomplish by operating on the generated html. The idea is that before rendering, you pass a function to call during/after rendering to html that will be called on any rendered html elements that match the requested CSS selector. The main problem I see here is that I'm not aware of any good html libraries for R.

On a related note, I've developed a tool for generating R packages for java code and jsoup is quite nice to use, and the rJava syntax wouldn't make it hard for users. On the other hand, it'd add a bit of baggage. I'm not sure how much that troubles you.

ramnathv commented 11 years ago

I like your description of a hook. Can you provide me with a few concrete use cases for it, so that I can think about what would be the easiest way to implement it.

Currently, I do all post-processing in Javascript, since it is much easier manipulating the DOM with libraries like jQuery. The same can be done statically in R, using packages selectr and XML.

Once you provide me with some use cases, I can see how this would work.

jamiefolson commented 11 years ago

The one that sticks out in my mind is exactly the one you mentioned for reveal.js, converting all <li> in a <ul> with class incremental to have class fragment.

slidify$set_hook("ul.incremental > li",function(elem) { # syntax here will
depend on how html is passed through
    add_class(elem,"fragment") })

It actually shouldn't be too hard to map jquery syntax more or less into R syntax using $ for S4 classes to chain things.

Jamie Olson

On Tue, Mar 19, 2013 at 9:57 AM, Ramnath Vaidyanathan < notifications@github.com> wrote:

I like your description of a hook. Can you provide me with a few concrete use cases for it, so that I can think about what would be the easiest way to implement it.

Currently, I do all post-processing in Javascript, since it is much easier manipulating the DOM with libraries like jQuery. The same can be done statically in R, using packages selectr and XML.

Once you provide me with some use cases, I can see how this would work.

— Reply to this email directly or view it on GitHubhttps://github.com/ramnathv/slidify/issues/179#issuecomment-15115348 .

ramnathv commented 11 years ago

Actually, there are two packages, which make this easy. Here is a function I cooked up quickly to achieve the use case you presented. doc is the html parsed by the XML package

make_li_fragments <- function(doc){
  require(XML)     # for dom manipulation
  require(selectr)  # for jquery selectors instead of xpath
  li_fragments = querySelectorAll(doc, 'ul.incremental > li')
  lapply(li_fragments, addAttributes, class = 'fragment')
}

I can throw this into another function that parses the HTML, applies the hooks and saves the resulting HTML file.

hooks = list(make_li_fragments)
post_process_slides <- function(htmlFile, hooks){
  doc <- htmlParse('index.html')
  for (hook in hooks){
    hook(doc)
  }
  cat(saveXML(doc), htmlFIle)

The question now is how to make this work in an elegant fashion.

On a related note, I would prefer to do DOM manipulation in Javascript using jQuery since it has pretty sophisticated functionality and is dynamic. So, the same thing can be achieved using a jQuery one-liner (which is what I am currently doing).

$('ul.incremental > li').addClass('fragment')

Maybe it is time for someone to write rjQuery :+1:

jamiefolson commented 11 years ago

Yeah, jQuery makes things pretty simple. I would prefer it if the basic document composition stuff (as opposed to the dynamic manipulation performed by the framework) were done in the "backend". For one thing, it's a bit harder to debug any issues if the converted document is not expected to conform to the framework's requirements (e.g. fragment class vs incremental). How hard would it be to apply that jQuery statement during slidify processing as opposed to just adding it to the html header?

In terms of syntax, I think the closest you could get in R to the above would be:

`%$%`('ul.incremental > li')$addClass('fragment')

That doesn't look to bad to me, though.

ramnathv commented 11 years ago

I agree that the most basic manipulations can be done at the backend. In order to allow the user to harness the full power of the XML package, I could allow the user to define a set of hooks, either as a parameter to the slidify function, or inside the document using slidify$set(hooks = ....). This should be easy to implement, since all that is required for me to do is to run post_process_slides after I slidify the document.

Standard transformations like the one you mentioned above, should be integrated with the framework. I am thinking I can add a hooks folder to all frameworks, which will contain framework specific system hooks to run after the document is slidified. Users can specify additional hooks in assets/hooks folder, which would be automatically run.

The more I think about it, it is clearly post-processing and there are multiple ways to implement it. The question is what is the cleanest.

Regarding jQuery like syntax, I think that calls for a different package, since the functionality is orthogonal to what Slidify does. XML package is good, but the syntax is hard. Maybe, we can write a wrapper around XML that conforms to jQuery syntax. Interested?

jamiefolson commented 11 years ago

Sure. I've got some time today. I'll put something together. On a marginally related note, is there any way to specify that R package dependencies can be found on github, so they're automatically downloaded and installed? In your instructions, you tell people to explicitly install the libraries and base packages, so I'm guessing not.

ramnathv commented 11 years ago

Great! jQuery is one of the javascript libraries I am very familiar with, so I can chip in with some help.

No, it is not possible to specify dependencies on github. I raised this issue on devtools, and the response I got was that they don't have a good way to do that right now.

jamiefolson commented 11 years ago

I'm a bit confused now by the code you posted, since XML claims that the addAttributes and related functions do not modify the nodes they operate on. Would your example work as written or should it be more like:

make_li_fragments <- function(doc){
  require(XML)     # for dom manipulation
  require(selectr)  # for jquery selectors instead of xpath
  li_fragments = querySelectorAll(doc, 'ul.incremental > li')
  new_li_fragments = lapply(li_fragments, XML:::addAttributes, class = 'fragment')
  XML:::replaceNodes
}

EDIT: I figured it out. Even though the XML docs say it doesn't modify the node, it does modify internal nodes.

ramnathv commented 11 years ago

You figured it out. The XML package modifies the DOM in place.

jamiefolson commented 11 years ago

I committed a bare minimum working package as rQuery.

You can chain commands:

`%$%`(doc,"body")$select("slide")$select("p")$html()

Right now it's creating new S4 classes, but I don't think that's actually necessary. I think I can just extend XMLAbstractNode and XMLNodeSet instead. Anyway, let me know if there's any obvious flaws. It turns out XML doesn't always edit nodes in-place, but it does when using any of the *Internal* classes which use external pointers. Since htmlParse uses those internal classes, it shouldn't really matter as long as you don't use htmlTreeParse.

Let me know what functions from jQuery you're using and I'll add those and add some documentation. It should be quite easy to implement additional jQuery functionality, but there's no need unless it's used.

EDIT: I changed things around to use S4 dispatch, but the api is the same. I think I'm happy enough with the way things are, so I'll just add in some documentation and tests and decide what to export. All you need to do to add more features is define an S4 method rQuery.name for signature rQueryNode (and/or rQueryNodeSet) and then the $ method will dispatch x$name to call the rQuery.name method you defined. I did it this because the XMLAbstractNode and XMLNodeSet classes used by XML are S3 classes which don't allow you to define the $ method, and I'm forcing the rQuery. prefix on method names because otherwise you're fighting against names in the base namespace(ie overriding attr and class is not allowed).

ramnathv commented 11 years ago

Great. I will take a closer look when I find time. I went through my jQuery codebase, and realized that there are only a handful of functions which manipulate the DOM statically. The rest are required to be dynamic and hence have to be in JS.

Given that, I am thinking if it would be worthwhile to develop rQuery purely to support Slidify. However, given the emergence of Shiny, which brings R to the server, I can see rQuery as a powerful add-on that can allow users to add interactive functionality using a friendly syntax. So developing rQuery as a standalone package to support DOM manipulation would be very useful.

Let me know your thoughts.