home
readme
diff
tree
note
docs
0commit 4bfba076120f389994fc46a98e8b7a2622314400
1Author: Mason Wright <mason@lakefox.net>
2Date: Mon Jun 16 20:44:42 2025 -0600
3
4 Off to a better start on the parser
5
6diff --git a/element.cc b/element.cc
7index 059e344..08f9888 100755
8--- a/element.cc
9+++ b/element.cc
10@@ -315,62 +315,34 @@ std::unordered_map<std::string, std::string> parseAttributes(std::vector<std::st
11 return attrs;
12 }
13
14+enum Type {
15+ TEXT,
16+ NODE
17+};
18+
19+struct Token {
20+ Type type;
21+ std::string data;
22+};
23+
24 // !TODO: Make a html string parser as well
25 std::unique_ptr<Node> parseStream(std::istream& inputStream) {
26 std::unique_ptr<Node> root = std::make_unique<Node>();
27 root->setTagName("root");
28
29- std::string line;
30 Node* currentNode = root.get();
31- // !ISSUE: test
32- //
33+
34 bool inTag = false;
35 bool escaped = false;
36 bool inQuote = false;
37 char quoteType = '"';
38- bool isClosing = false;
39
40- std::vector<std::string> tokens;
41- std::string word = "";
42+ std::vector<Token> tokens;
43
44 char current;
45-
46- // !TODO: self closing tags <br> <br/> <input />
47- // + also tags with no content
48 while (inputStream.get(current)) {
49- std::cout << "--------" << std::endl;
50- std::cout << current << std::endl;
51- if (isClosing && current != '>') {
52- std::cout << "Continuing no >" << std::endl;
53- continue;
54- } else if (isClosing && current == '>') {
55- std::cout << "Continuing at >" << std::endl;
56- isClosing = false;
57- continue;
58- }
59-
60 if (inTag) {
61- std::cout << "In Tag" << std::endl;
62- if (current == '/') {
63- std::cout << "End Tag" << std::endl;
64- isClosing = true;
65- } else if (current == '>' && !escaped) {
66- tokens.push_back(word);
67- std::cout << "Creating Element: " << tokens[0] << std::endl;
68- // Add the last word
69-
70- word = "";
71- inTag = false;
72- // Build the element
73- currentNode = currentNode->createElement(tokens[0]);
74- auto attrs = parseAttributes(tokens);
75- for (auto const& pair : attrs) {
76- // All attributes are stored as strings so we can just throw them in
77- currentNode->setAttribute(pair.first, pair.second);
78- }
79-
80-
81- } else if ((current == '"' || current == '\'') && !escaped) {
82+ if ((current == '"' || current == '\'') && !escaped) {
83 if (inQuote && current == quoteType) {
84 inQuote = false;
85 } else if (!inQuote) {
86@@ -378,32 +350,42 @@ std::unique_ptr<Node> parseStream(std::istream& inputStream) {
87 quoteType = current;
88 }
89 }
90- if (current == ' ' && !inQuote) {
91- tokens.push_back(word);
92- word = "";
93- } else {
94- word += current;
95+ if (current == '>' && !inQuote && !escaped) {
96+ inTag = false;
97+ // Even if the next tag is next still add a text tag as we can just check if its empty and remove it
98+ // this check would still have to be done either way so we aren't wasting anything
99+ std::string data = "";
100+ Token newToken = Token{TEXT, data};
101+ tokens.push_back(newToken);
102+ // for closing tags, use them as a checksum and to manage the position in the tree
103+ continue;
104 }
105-
106- } else if (current != '<' && current != '>') {
107- std::cout << "Adding innerText to " << currentNode->getTagName() <<":"<< current << std::endl;
108- // Outside tag add to innerText
109- currentNode->setInnerText(currentNode->getInnerText() + current);
110- }
111-
112- if (current == '<' && !escaped) {
113- tokens.clear();
114- word = "";
115+ } else if (!escaped && current == '<') {
116 inTag = true;
117+ std::string data = "";
118+ Token newToken = Token{NODE, data};
119+ tokens.push_back(newToken);
120+ // Heres where you actually make the text node and above the real nodes
121+ // can also prob make the vector of tokens a single variable
122+ continue;
123 }
124-
125+
126 escaped = false;
127
128 if (current == '\\') { //'
129 escaped = true;
130+ continue;
131+ }
132+
133+ if (!tokens.empty()) {
134+ tokens.back().data += current;
135 }
136 }
137
138+ for (auto t : tokens) {
139+ std::cout << t.type << t.data << std::endl;
140+ }
141+
142 return root;
143 }
144
145diff --git a/index.html b/index.html
146index 9e83031..47a71eb 100644
147--- a/index.html
148+++ b/index.html
149@@ -1,10 +1,6 @@
150-<html
151- lang="en thie i\"s\" a test"
152- contentEditable
153->
154-<h1></h1>
155+<html>
156+ <h1 id="<"></h1>
157 <p id="test">
158 this is a test
159- <input type='text'/>
160 </p>
161 </html>
162diff --git a/main b/main
163index 6c1b027..445d49b 100755
164Binary files a/main and b/main differ
165diff --git a/test.cc b/test.cc
166deleted file mode 100644
167index a53d635..0000000
168--- a/test.cc
169+++ /dev/null
170@@ -1,459 +0,0 @@
171-#include <iostream>
172-#include <string>
173-#include <vector>
174-#include <memory>
175-#include <map>
176-#include <sstream> // For stringstream input, useful for testing
177-#include <fstream> // For file input
178-
179-// Forward declarations to define Node and parseAttributes before parseHtmlStream
180-class Node;
181-std::map<std::string, std::string> parseAttributes(const std::vector<std::string>& tokens);
182-
183-/**
184- * @brief Represents a node in the HTML Document Object Model (DOM) tree.
185- * Can be an element node (e.g., <div>), a text node (innerText), or the root.
186- */
187-class Node {
188-public:
189- std::string tagName; // The tag name of the element (e.g., "div", "h1")
190- std::string innerText; // Text content directly inside this node
191- std::map<std::string, std::string> attributes; // Map of attribute key-value pairs
192- std::vector<std::unique_ptr<Node>> children; // Vector of unique pointers to child nodes
193- Node* parent; // Raw pointer to the parent node (ownership managed by unique_ptr in children)
194-
195- /**
196- * @brief Constructor for Node. Initializes parent to nullptr.
197- */
198- Node() : parent(nullptr) {}
199-
200- // --- Setters ---
201- void setTagName(const std::string& name) { tagName = name; }
202- void setInnerText(const std::string& text) { innerText = text; }
203- void setAttribute(const std::string& key, const std::string& value) { attributes[key] = value; }
204-
205- // --- Getters ---
206- const std::string& getTagName() const { return tagName; }
207- const std::string& getInnerText() const { return innerText; }
208- const std::map<std::string, std::string>& getAttributes() const { return attributes; }
209-
210- /**
211- * @brief Creates a new child node for the current node.
212- * Sets the new node's tag name and its parent pointer to `this`.
213- * Transfers ownership of the new node to the `children` vector.
214- * @param name The tag name of the new element (e.g., "p", "a").
215- * @return A raw pointer to the newly created child node.
216- */
217- Node* createElement(const std::string& name) {
218- std::unique_ptr<Node> newNode = std::make_unique<Node>();
219- newNode->setTagName(name);
220- newNode->parent = this; // Set parent pointer for the new node
221- Node* rawPtr = newNode.get(); // Get raw pointer before moving ownership
222- children.push_back(std::move(newNode)); // Transfer ownership to the children vector
223- return rawPtr;
224- }
225-
226- /**
227- * @brief Prints the HTML tree structure recursively for debugging purposes.
228- * Includes indentation for better readability.
229- * @param indent The current indentation level.
230- */
231- void print(int indent = 0) const {
232- // Print indentation for the current node
233- for (int i = 0; i < indent; ++i) std::cout << " ";
234-
235- // Print the opening tag and its attributes
236- std::cout << "<" << tagName;
237- for (const auto& attr : attributes) {
238- std::cout << " " << attr.first << "=\"" << attr.second << "\"";
239- }
240- std::cout << ">";
241-
242- // Print inner text if it exists
243- if (!innerText.empty()) {
244- std::cout << innerText;
245- }
246-
247- // Determine if a closing tag is required.
248- // If the node has children or inner text, it definitely needs a closing tag.
249- // Even for empty tags (like <h1></h1> with no content), a closing tag is required.
250- bool requiresClosingTag = true; // Assuming all elements created require a closing tag
251- // unless explicitly handled as self-closing in parser.
252-
253- // If the node has content (text or children), print a newline for better formatting.
254- // Then recursively print children, followed by the closing tag.
255- if (!innerText.empty() || !children.empty()) {
256- std::cout << std::endl; // Newline after opening tag if content/children follow
257-
258- // Recursively print all child nodes
259- for (const auto& child : children) {
260- child->print(indent + 1);
261- }
262-
263- // Print indentation for the closing tag
264- for (int i = 0; i < indent; ++i) std::cout << " ";
265- std::cout << "</" << tagName << ">" << std::endl;
266- } else if (requiresClosingTag) {
267- // For truly empty tags (e.g., <h1></h1>), print newline and then its closing tag
268- std::cout << std::endl;
269- for (int i = 0; i < indent; ++i) std::cout << " ";
270- std::cout << "</" << tagName << ">" << std::endl;
271- } else {
272- // This case handles conceptual self-closing for elements that don't need a closing tag
273- // (e.g. <br/>). In the parser, `currentNode` immediately moves up for these.
274- // So, for the `print` function, if it reaches this branch, it means the node was effectively "closed".
275- std::cout << std::endl; // Just print a newline after the self-closing tag for formatting.
276- }
277- }
278-};
279-
280-/**
281- * @brief Helper function to parse attributes from a vector of tokens.
282- * It expects tokens to be either `key=value` (with or without quotes) or just `key` (for boolean attributes).
283- * @param tokens A vector of strings, where the first element is the tag name, and subsequent elements are attributes.
284- * @return A map of attribute keys to their values.
285- */
286-std::map<std::string, std::string> parseAttributes(const std::vector<std::string>& tokens) {
287- std::map<std::string, std::string> attrs;
288- if (tokens.empty()) return attrs;
289-
290- // Skip the first token which is the tag name itself
291- for (size_t i = 1; i < tokens.size(); ++i) {
292- const std::string& token = tokens[i];
293- size_t eqPos = token.find('='); // Find the position of '='
294-
295- if (eqPos != std::string::npos) {
296- // Attribute is in `key=value` format
297- std::string key = token.substr(0, eqPos);
298- std::string value = token.substr(eqPos + 1);
299-
300- // Remove quotes if the value is enclosed in them (e.g., value="my value")
301- if (!value.empty() && (value.front() == '"' || value.front() == '\'')) {
302- if (value.length() >= 2 && value.back() == value.front()) { // Ensure matching quotes
303- value = value.substr(1, value.length() - 2);
304- }
305- }
306- attrs[key] = value;
307- } else {
308- // Attribute is a boolean attribute or a key without an explicit value (e.g., `disabled`)
309- attrs[token] = "";
310- }
311- }
312- return attrs;
313-}
314-
315-/**
316- * @brief Core HTML parsing logic that reads character by character from an input stream.
317- * @param inputStream The input stream (e.g., `ifstream` for file, `stringstream` for string).
318- * @return A unique_ptr to the root Node of the parsed HTML tree.
319- */
320-std::unique_ptr<Node> parseHtmlStream(std::istream& inputStream) {
321- std::unique_ptr<Node> root = std::make_unique<Node>();
322- root->setTagName("root"); // Create a conceptual root node for the document
323-
324- Node* currentNode = root.get(); // Pointer to the currently active node in the tree
325-
326- std::string currentTokenPart = ""; // Buffer for collecting tag names, attribute keys, or partial attribute values
327- std::string textBuffer = ""; // Buffer for collecting inner text content
328-
329- bool inTag = false; // True when parser is inside '<' and '>' of a tag
330- bool inClosingTag = false; // True when parsing a closing tag (e.g., after `</`)
331- bool inAttributeValue = false; // True when parsing a quoted attribute value (e.g., `value="...")
332- char attributeQuoteType = ' '; // Stores the type of quote (`'` or `"`) used for the attribute value
333-
334- std::vector<std::string> tagTokens; // Stores the tag name and all its attributes as individual tokens
335-
336- char current; // Current character being processed
337-
338- // Loop through the input stream character by character
339- while (inputStream.get(current)) {
340- if (inTag) {
341- // --- Parser is currently inside an HTML tag (`<...>` or `</...>`) ---
342- if (inAttributeValue) {
343- // Currently parsing content within an attribute's quoted value
344- if (current == attributeQuoteType) {
345- // End of the attribute value (closing quote found)
346- inAttributeValue = false;
347- tagTokens.push_back(currentTokenPart); // Add the complete attribute value to tokens
348- currentTokenPart = ""; // Reset buffer for next part
349- } else {
350- currentTokenPart += current; // Continue accumulating the attribute value
351- }
352- } else if (current == ' ' || current == '\t' || current == '\n' || current == '\r') {
353- // Whitespace: separates tag name, attributes, or self-closing markers
354- if (!currentTokenPart.empty()) {
355- tagTokens.push_back(currentTokenPart); // Add the accumulated part (tag name or attribute key/value)
356- currentTokenPart = ""; // Reset buffer
357- }
358- } else if (current == '=' && !inClosingTag) {
359- // Encountered an equals sign (for `key=value` attributes)
360- if (!currentTokenPart.empty()) {
361- tagTokens.push_back(currentTokenPart); // Push the attribute key
362- currentTokenPart = "";
363- }
364- currentTokenPart += current; // Add the '=' to the current part (it will be combined with value later)
365- } else if (current == '"' || current == '\'') {
366- // Start of an attribute value (quoted)
367- // If there's an accumulated part, it should be the key or `key=`
368- if (!currentTokenPart.empty()) {
369- tagTokens.push_back(currentTokenPart);
370- }
371- currentTokenPart = ""; // Clear for the actual value
372- inAttributeValue = true;
373- attributeQuoteType = current; // Store the quote type
374- } else if (current == '/') {
375- // Encountered a slash (`/`). Could be for:
376- // 1. Self-closing tag (e.g., `<br/>`, `<input />`)
377- // 2. Start of a closing tag (e.g., `</div`) - this is already handled by `inClosingTag` check before `inTag` starts
378- if (!currentTokenPart.empty()) {
379- tagTokens.push_back(currentTokenPart);
380- }
381- currentTokenPart = ""; // Reset for '/' itself
382- currentTokenPart += current; // Add '/' as a token part
383- } else if (current == '>') {
384- // --- End of a tag (`>` found) ---
385- inTag = false; // Exiting the tag processing state
386-
387- // If there's any remaining accumulated part, add it to tokens
388- if (!currentTokenPart.empty()) {
389- tagTokens.push_back(currentTokenPart);
390- currentTokenPart = "";
391- }
392-
393- if (inClosingTag) {
394- // This was a closing tag (e.g., `</h1>`)
395- if (!tagTokens.empty()) {
396- std::string closingTagName = tagTokens[0];
397- // Trim trailing '/' if it was parsed as part of the tag name (e.g. `br/` due to tokenization)
398- if (!closingTagName.empty() && closingTagName.back() == '/') {
399- closingTagName.pop_back();
400- }
401-
402- // Check if the closing tag matches the current node's tag name
403- if (currentNode->getTagName() == closingTagName) {
404- if (currentNode->parent) {
405- currentNode = currentNode->parent; // Move up to the parent node
406- } else {
407- // Error: Tried to close the root node or unmatched closing tag without a parent
408- std::cerr << "Warning: Attempted to close root tag or unmatched closing tag: </" << closingTagName << ">" << std::endl;
409- }
410- } else {
411- // Mismatched closing tag (e.g., `<div><span></div>`)
412- std::cerr << "Error: Mismatched closing tag. Expected </" << currentNode->getTagName()
413- << ">, got </" << closingTagName << ">" << std::endl;
414- // For robustness, you might choose to still move up or try to find a matching ancestor.
415- // For this simple parser, we'll still attempt to move up, assuming it's a parse error.
416- if (currentNode->parent) {
417- currentNode = currentNode->parent;
418- }
419- }
420- } else {
421- std::cerr << "Warning: Empty closing tag detected (e.g., </>)!" << std::endl;
422- }
423- inClosingTag = false; // Reset the closing tag state
424- } else {
425- // This was an opening tag (e.g., `<h1>`, `<div id="x">`)
426- // Or a self-closing tag (e.g., `<br/>`, `<img />`)
427- if (!tagTokens.empty()) {
428- std::string tagName = tagTokens[0]; // The first token is the tag name
429- bool isSelfClosing = false;
430-
431- // Check if the tag is self-closing
432- // 1. If the tag name itself ends with '/' (e.g., `<br/>` where `br/` is the token)
433- if (!tagName.empty() && tagName.back() == '/') {
434- isSelfClosing = true;
435- tagName.pop_back(); // Remove the '/' from the tag name
436- }
437- // 2. If the last token is just "/" (e.g., `<input />` where `/` is a separate token)
438- else if (!tagTokens.empty() && tagTokens.back() == "/") {
439- isSelfClosing = true;
440- tagTokens.pop_back(); // Remove the "/" token from the list
441- }
442-
443- // Create the new node as a child of the current node
444- Node* newNode = currentNode->createElement(tagName);
445- // Parse attributes from the collected tokens
446- auto attrs = parseAttributes(tagTokens);
447- for (auto const& pair : attrs) {
448- newNode->setAttribute(pair.first, pair.second);
449- }
450- currentNode = newNode; // Move `currentNode` down to the newly created element
451-
452- if (isSelfClosing) {
453- // For self-closing tags, immediately move back up to the parent
454- // as they do not contain children or inner text.
455- if (currentNode->parent) {
456- currentNode = currentNode->parent;
457- }
458- }
459- } else {
460- std::cerr << "Warning: Empty or malformed opening tag detected: < >" << std::endl;
461- }
462- }
463- tagTokens.clear(); // Clear tokens for the next tag
464- textBuffer = ""; // Clear any accumulated text before a new tag or content starts
465- } else {
466- // Accumulate characters for the tag name or an attribute key/value part
467- currentTokenPart += current;
468- }
469- } else {
470- // --- Parser is currently outside a tag, processing text content or looking for new tags ---
471- if (current == '<') {
472- // A new tag is starting.
473- // If there's accumulated text, assign it as innerText to the current node
474- if (!textBuffer.empty()) {
475- // Trim leading/trailing whitespace/newlines from the text content
476- size_t first = textBuffer.find_first_not_of(" \t\n\r");
477- size_t last = textBuffer.find_last_not_of(" \t\n\r");
478- if (std::string::npos != first) {
479- currentNode->setInnerText(textBuffer.substr(first, (last - first + 1)));
480- }
481- textBuffer = ""; // Reset text buffer for next content
482- }
483-
484- // Peek at the next character to determine the type of tag
485- char nextChar;
486- if (inputStream.get(nextChar)) { // Attempt to read the next character without consuming it fully yet
487- if (nextChar == '/') {
488- // It's a closing tag (e.g., `</div`)
489- inClosingTag = true;
490- inTag = true; // Enter the tag parsing state
491- } else if (nextChar == '!') {
492- // It's a comment (`<!-- ... -->`) or DOCTYPE (`<!DOCTYPE ...>`)
493- // Simple skipping logic: read until the next `>`
494- while (inputStream.get(current) && current != '>');
495- continue; // Continue to the next character in the main loop
496- } else {
497- // It's a regular opening tag (e.g., `<div>`)
498- inTag = true; // Enter the tag parsing state
499- currentTokenPart += nextChar; // Add the actual tag name character
500- }
501- } else {
502- // Unexpected end of file after '<'
503- std::cerr << "Warning: Unexpected EOF after '<'" << std::endl;
504- break; // Exit parsing loop
505- }
506- } else {
507- // Accumulate characters as inner text content for the current node
508- textBuffer += current;
509- }
510- }
511- }
512-
513- // After the loop, if there's any remaining text in the buffer, assign it
514- if (!textBuffer.empty()) {
515- size_t first = textBuffer.find_first_not_of(" \t\n\r");
516- size_t last = textBuffer.find_last_not_of(" \t\n\r");
517- if (std::string::npos != first) {
518- currentNode->setInnerText(textBuffer.substr(first, (last - first + 1)));
519- }
520- }
521-
522- return root; // Return the root of the parsed HTML tree
523-}
524-
525-/**
526- * @brief Parses an HTML file and constructs a DOM tree.
527- * @param file The path to the HTML file.
528- * @return A unique_ptr to the root Node of the parsed HTML tree, or nullptr if the file cannot be opened.
529- */
530-std::unique_ptr<Node> parserdocument(std::string file) {
531- std::ifstream inputFile(file); // Open the specified file
532-
533- if (!inputFile.is_open()) {
534- std::cerr << "Unable to open: " << file << std::endl;
535- return nullptr; // Return nullptr if file opening fails
536- }
537-
538- return parseHtmlStream(inputFile); // Use the core parsing logic
539-}
540-
541-
542-// --- Main function for demonstration and testing ---
543-int main() {
544- // Test Case 1: Empty tag <h1></h1>
545- std::cout << "--- Test Case 1: Empty tag <h1></h1> ---" << std::endl;
546- std::string html1 = "<h1></h1>";
547- std::stringstream ss1(html1);
548- std::unique_ptr<Node> root1 = parseHtmlStream(ss1);
549- if (root1) {
550- root1->print();
551- }
552- std::cout << "\n";
553-
554- // Test Case 2: Tag with text content <div>Hello World</div>
555- std::cout << "--- Test Case 2: Tag with text content <div>Hello World</div> ---" << std::endl;
556- std::string html2 = "<div>Hello World</div>";
557- std::stringstream ss2(html2);
558- std::unique_ptr<Node> root2 = parseHtmlStream(ss2);
559- if (root2) {
560- root2->print();
561- }
562- std::cout << "\n";
563-
564- // Test Case 3: Nested tags with mixed content
565- std::cout << "--- Test Case 3: Nested tags with mixed content ---" << std::endl;
566- std::string html3 = "<body><p>This is <b>bold</b> text.</p></body>";
567- std::stringstream ss3(html3);
568- std::unique_ptr<Node> root3 = parseHtmlStream(ss3);
569- if (root3) {
570- root3->print();
571- }
572- std::cout << "\n";
573-
574- // Test Case 4: Tags with attributes and self-closing tags
575- std::cout << "--- Test Case 4: Tags with attributes and self-closing tags ---" << std::endl;
576- std::string html4 = "<div id=\"main\" class='container'><img src=\"image.jpg\" alt=\"My Image\" /><br></div>";
577- std::stringstream ss4(html4);
578- std::unique_ptr<Node> root4 = parseHtmlStream(ss4);
579- if (root4) {
580- root4->print();
581- }
582- std::cout << "\n";
583-
584- // Test Case 5: Empty tag with whitespace between
585- std::cout << "--- Test Case 5: Empty tag with whitespace between <h1> </h1> ---" << std::endl;
586- std::string html5 = "<h1> </h1>";
587- std::stringstream ss5(html5);
588- std::unique_ptr<Node> root5 = parseHtmlStream(ss5);
589- if (root5) {
590- root5->print();
591- }
592- std::cout << "\n";
593-
594- // Test Case 6: Malformed closing tag
595- std::cout << "--- Test Case 6: Malformed closing tag (mismatch) ---" << std::endl;
596- std::string html6 = "<div><span></div>"; // Mismatched closing tag
597- std::stringstream ss6(html6);
598- std::unique_ptr<Node> root6 = parseHtmlStream(ss6);
599- if (root6) {
600- root6->print();
601- }
602- std::cout << "\n";
603-
604- // Test Case 7: Comment and DOCTYPE
605- std::cout << "--- Test Case 7: Comment and DOCTYPE ---" << std::endl;
606- std::string html7 = "<!DOCTYPE html>\n<!-- This is a comment --><body>Content</body>";
607- std::stringstream ss7(html7);
608- std::unique_ptr<Node> root7 = parseHtmlStream(ss7);
609- if (root7) {
610- root7->print();
611- }
612- std::cout << "\n";
613-
614- // Example of using parserdocument function with a file (requires a test.html file)
615- // std::cout << "--- Test Case 8: Parsing from file (test.html) ---" << std::endl;
616- // // Create a dummy test.html file for this test
617- // std::ofstream outfile("test.html");
618- // outfile << "<html><body><p>Hello from file!</p></body></html>";
619- // outfile.close();
620- //
621- // std::unique_ptr<Node> root8 = parserdocument("test.html");
622- // if (root8) {
623- // root8->print();
624- // } else {
625- // std::cout << "Failed to parse test.html" << std::endl;
626- // }
627-
628- return 0;
629-}