How do i extract a list of specific dom-attributes in a nested dom tree?

Viewed 146

I would like to traverse and collect the node with the data-table attribute, extract its value, then obtain its child with the data-field or additional attributes, and extract its value which would be saved as a list.

From the Html example below, I have set up anchor points of dom-attributes in a dom-tree, which is intended to be converted into a model structure after traversing and extracting them.

    <body>
        <div class="wrap" data-table="page"> Sample Text <p data-field="heading" class="format" >Welcome to this page</p>
            <div class="flex-grid generic-card">
                <h1 class="card " data-field="intro">Text </h1>
               <div class="card " data-field="body"></div>
            </div>
        </div>

I am expecting the final result to be in a form of a flat list with something similar to (page . ("title" "intro" "body"))

with the following code, I'm able to traverse the node and extract 'data-table' but the problem is, I'm not able to extract data-field attached to data-table. I unsuccessfully tried to use the recursion approach which consists of repeating the example of 'dom-struct' and dom-search function. what I noticed is libxml-parse-html-region'' returns empty strings with newlines alongside the dom-nodes after parsing through the dom tree which generates an error.

This code's purpose is to extract the node from the tree recursively

(require 'dom)
(defun dom-struct (x)
  (print (dom-attr x 'data-table)) ; extract the data-table attribute
  (print (dom-tag (dom-node x)))        ;extract dom-tag
  (print (dom-children (dom-node x))) ; extract dom-children attached to a node but don't know how to extract data-field attribute
  (print (dom-search (dom-children (dom-node x)) (lambda (node) (assq 'data-attribute (cadr node)))))
  (mapconcat #'dom-struct (dom-children (dom-node x)) ""))

(defun macro-structify (tag-entries)
  (with-temp-buffer
    (insert tag-entries)
    (let* ((mytags (libxml-parse-html-region (point-min) (point-max))))
      (dom-struct (car (dom-by-tag mytags 'body))))))

(let ((myskel "<html>
    <head>
        <title>Demo: Gradient Slide</title>
    </head>
    <link href=\"https://fonts.googleapis.com/css?family=Nunito+Sans\" rel=\"stylesheet\">
    <link rel=\"stylesheet\" href=\"dist/build.css\">
    <body data-table=\"layout\">
        <header data-field=\"title\">
            <h1>Skeleton Screen</h1>
        </header>
        <div class=\"wrap\" data-table=\"page\"> Sample Text <p data-field=\"heading\" class=\"format\" data-attribute=\"somethingsomething\">Welcome to this page</p>
            <div class=\"flex-grid generic-card\">
                <div class=\"card loading\" data-field=\"intro\">Text </div>
               <div class=\"card loading\" data-field=\"body\"></div>
            </div>
        </div>
    </body>
</html>"))
  (macro-structify myskel))
1 Answers

Here's a solution using esxml-query from the esxml package. It looks for all nodes with a data-field attribute that are children of a div node with a data-table attribute, then collects their attribute values into a list.

(require 'dom)
(require 'esxml-query)

(let* ((myskel "<html>
    <head>
        <title>Demo: Gradient Slide</title>
    </head>
    <link href=\"https://fonts.googleapis.com/css?family=Nunito+Sans\" rel=\"stylesheet\">
    <link rel=\"stylesheet\" href=\"dist/build.css\">
    <body data-table=\"layout\">
        <header data-field=\"title\">
            <h1>Skeleton Screen</h1>
        </header>
        <div class=\"wrap\" data-table=\"page\"> Sample Text <p data-field=\"heading\" class=\"format\" data-attribute=\"somethingsomething\">Welcome to this page</p>
            <div class=\"flex-grid generic-card\">
                <div class=\"card loading\" data-field=\"intro\">Text </div>
               <div class=\"card loading\" data-field=\"body\"></div>
            </div>
        </div>
    </body>
</html>")
       (dom (with-temp-buffer
              (insert myskel)
              (libxml-parse-html-region (point-min) (point-max))))
       (table-node (esxml-query "div[data-table]" dom))
       (model-nodes (esxml-query-all "[data-field]" table-node))
       (model-data-table (dom-attr table-node 'data-table))
       (model-data-fields (mapcar (lambda (node) (dom-attr node 'data-field)) model-nodes)))
  (cons model-data-table model-data-fields))
  ;; => ("page" "heading" "intro" "body")

The result is different from what you've specified for several reasons:

  • The whole HTML snippet contains a body tag with a data-table attribute before a div tag with a data-table attribute, but your HTML fragment looks at the latter, so I've changed the code to look for a div tag with a data-table attribute
  • There is a header tag with a data-field attribute set to "title" (the expected field), but it's part of the body tag with the data-table attribute set to "layout", not the div tag with the data-table attribute set to "page" (the actual field)
  • The remaining fields are as expected, but printed differently than specified, because in many Lisp languages, (foo . (bar baz)) is identical to (foo bar baz) and usually printed in the latter form
Related