This application can be used to detect errors in HTML tags. It takes the HTML code as input and highlights tags in the following cases:
<b><i></b></i>
. Text inside comments, as well as php directives enclosed in <? ?>
tags are ignored. The same goes for tag attributes.
The app functions are written in C++17 and the user interface was built with Qt (Qt version 5.13.0, MinGW 7.3.0). It was made for personal use, so there's lots of room for improvement in terms of app efficiency.
Download(zip) contains the program and source code. The executable is made for Windows OS – more versions could be easily created for other platforms, using the source files. Since the application was built as a standalone file using a static version of Qt, it can be used without any setup and doesn't require any .dll files. However, this also led to the file size being bigger than expected for such a small application (17MB).
//Knowledgedump.org - HTML Checker #ifndef HTML_CHECKER_H #define HTML_CHECKER_H #include <QTabWidget> #include <QTextStream> #include <QFileDialog> #include <QMessageBox> #include <QClipboard> #include <QTextCharFormat> #include <QTextCursor> namespace Ui { class HTML_Checker; } class HTML_Checker : public QTabWidget { Q_OBJECT //Meta object compiler (moc) macro. public: //Constructor & Destructor explicit HTML_Checker(QTabWidget *parent = nullptr); ~HTML_Checker(); private slots: void on_addST_button_clicked(); void on_addDT_button_clicked(); void on_save_button_clicked(); void on_load_button_clicked(); void on_paste_button_clicked(); void on_check_button_clicked(); void on_removeST_button_clicked(); void on_removeDT_button_clicked(); void on_reset_button_clicked(); void on_jump_button_clicked(); private: Ui::HTML_Checker *ui; void init(); void clean_string(QString&); void clean_stringList(QStringList&); void removeMatches_stringList(QStringList&, QStringList&); bool is_close_tag(QString&); QString convert_to_close_tag(QString); bool check_string(QString&, int&); //Default values for the HTML single and double tag lists. QStringList st_defaultList; QStringList dt_defaultList; //Actually used lists. QStringList st_list; QStringList dt_list; //List that contains currently open tags (converted to close tags). QStringList prev_tags; //The boolean is used to exit the check_string recursion, if a missing tag was detected. bool exit_flag; //Members to set formatting and errors. int err_count; QTextCursor selection; QTextCharFormat comment_format; QTextCharFormat php_format; QTextCharFormat error_format; QTextCharFormat neutral_format; QTextCharFormat unknown_format; QVector<int> err_locations; int flag1, flag2; }; #endif // HTML_CHECKER_H
//Knowledgedump.org - HTML Checker #include "html_checker.h" #include "ui_html_checker.h" //Constructor. HTML_Checker::HTML_Checker(QTabWidget *parent) : QTabWidget(parent), ui(new Ui::HTML_Checker) { //Build UI and initialize app. ui->setupUi(this); init(); //Configure UI. ui->input_textEdit->setTabStopDistance(40); ui->input_textEdit->setAcceptRichText(false); ui->st_fixedText->viewport()->setCursor(QCursor(Qt::ArrowCursor)); ui->dt_fixedText->viewport()->setCursor(QCursor(Qt::ArrowCursor)); //Set formatting for comments and errors. comment_format = ui->input_textEdit->currentCharFormat(); comment_format.setForeground(Qt::lightGray); php_format = ui->input_textEdit->currentCharFormat(); php_format.setForeground(Qt::blue); error_format = ui->input_textEdit->currentCharFormat(); error_format.setForeground(Qt::red); error_format.setFontUnderline(true); neutral_format = ui->input_textEdit->currentCharFormat(); neutral_format.setForeground(Qt::black); unknown_format = ui->input_textEdit->currentCharFormat(); unknown_format.setForeground(Qt::magenta); unknown_format.setFontUnderline(true); } //Destructor. HTML_Checker::~HTML_Checker() { delete ui; } //Utility function. Takes string and turns it into a tag of form <name>. //Removes any content after whitespaces and adds tag brackets <> if necessary. void HTML_Checker::clean_string(QString &str) { int index = str.indexOf(' '); if (index != -1) { str.truncate(index); } if (!str.startsWith('<')) { str.prepend('<'); } if (!str.endsWith('>')) { str.append('>'); } return; } //Utility function. Applies clean_string on all elements of a list. void HTML_Checker::clean_stringList(QStringList &list) { for (int i = 0; i < list.size(); ++i) { clean_string(list[i]); } return; } //Utility function. Removes any string in list2, which is also inside list1. void HTML_Checker::removeMatches_stringList(QStringList &list1, QStringList &list2) { for (int i = 0; i < list1.size(); ++i) { int index = list2.indexOf(list1[i]); if (index != -1) { list2.removeAt(index); } } return; } void HTML_Checker::init() { //Declare string lists that will contain all single and double tags. Strings are saved in <name> format. QStringList st_end; QStringList dt_end; //Set default lists of single ("self-closing") and double HTML tags. st_defaultList = QString("<!DOCTYPE>,<area>,<base>,<br>,<col>,<embed>,<hr>,<img>,<input>,<link>,<meta>,<param>," "<source>,<track>,<wbr>,<command>,<keygen>,<menuitem>").split(","); dt_defaultList = QString("<a>,<abbr>,<acronym,<address>,<applet>,<article>,<aside>,<audio>,<b>," "<basefont>,<bdi>,<bdo>,<big>,<blockquote>,<body>,<button>,<canvas>,<caption>,<center>," "<cite>,<code>,<colgroup>,<data>,<datalist>,<dd>,<del>,<details>,<dfn>,<dialog>,<dir>," "<div>,<dl>,<dt>,<em>,<fieldset>,<figcaption>,<figure>,<font>,<footer>,<form>,<frame>," "<frameset>,<h1>,<h2>,<h3>,<h4>,<h5>,<h6>,<head>,<header>,<html>,<i>,<iframe>,<ins>," "<kbd>,<label>,<legend>,<li>,<main>,<mark>,<meter>,<nav>,<noframes>,<noscript>,<object>," "<ol>,<optgroup>,<output>,<p>,<picture>,<pre>,<progress>,<q>,<rp>,<rt>,<ruby>,<s>," "<samp>,<script>,<section>,<select>,<small>,""<span>,<strike>,<strong>,<style>,<sub>," "<summary>,<sup>,<svg>,<table>,<tbody>,<td>,<template>,""<textarea>,<tfoot>,<th>," "<thead>,<time>,<title>,<tr>,<tt>,<u>,<ul>,<var>,<video>").split(","); //Append default string lists. st_end += HTML_Checker::st_defaultList; dt_end += HTML_Checker::dt_defaultList; //Load default.txt on program startup, if possible. If not, return and use defaults. QFile input("default.txt"); if (!input.open(QIODevice::ReadOnly | QIODevice::Text)) { ui->st_fixedText->setPlainText(st_end.join("\n")); ui->dt_fixedText->setPlainText(dt_end.join("\n")); st_list = st_end; dt_list = dt_end; return; } //Open input stream, save to strings. QTextStream stream(&input); //1st line should contain single tags. QStringList st(stream.readLine().split(",")); //2nd line should contain double tags. QStringList dt(stream.readLine().split(",")); //Clean up input (remove content after whitespace and add <> if necessary). clean_stringList(st); clean_stringList(dt); //Does any single tag element (from input) match a double tag from defaults? If yes, remove default one. removeMatches_stringList(st, dt_end); //Similarly for double tags. removeMatches_stringList(dt, st_end); //Append input tags to the respective category. Remove duplicate entries. st_end += st; st_end.removeDuplicates(); dt_end += dt; dt_end.removeDuplicates(); //Use updated lists and return. st_list = st_end; dt_list = dt_end; ui->st_fixedText->setPlainText(st_list.join("\n")); ui->dt_fixedText->setPlainText(dt_list.join("\n")); return; } void HTML_Checker::on_addST_button_clicked() { //Import tags from text line. QStringList input = ui->st_lineEdit->text().split(","); if (input.isEmpty()) { return; } //Turn input to tags of style <name>. clean_stringList(input); //Remove matching double tag entries. removeMatches_stringList(input, dt_list); //Append new tags to list. Remove duplicates. st_list += input; st_list.removeDuplicates(); //Reset fixed text boxes. ui->st_fixedText->setPlainText(st_list.join("\n")); ui->dt_fixedText->setPlainText(dt_list.join("\n")); return; } void HTML_Checker::on_removeST_button_clicked() { //Import single tags from text line. QStringList input = ui->st_lineEdit->text().split(","); if (input.isEmpty()) { return; } //Turn input to tags of style <name>. clean_stringList(input); //Remove matching single tag entries. removeMatches_stringList(input, st_list); //Reset fixed text box. ui->st_fixedText->setPlainText(st_list.join("\n")); return; } void HTML_Checker::on_addDT_button_clicked() { //Import tags from text line. QStringList input = ui->dt_lineEdit->text().split(","); if (input.isEmpty()) { return; } //Turn input to tags of style <name>. clean_stringList(input); //Remove matching single tag entries. removeMatches_stringList(input, st_list); //Append new tags to list. Remove duplicates. dt_list += input; dt_list.removeDuplicates(); //Reset fixed text boxes. ui->st_fixedText->setPlainText(st_list.join("\n")); ui->dt_fixedText->setPlainText(dt_list.join("\n")); return; } void HTML_Checker::on_removeDT_button_clicked() { //Import double tags from text line. QStringList input = ui->dt_lineEdit->text().split(","); if (input.isEmpty()) { return; } //Turn input to tags of style <name>. clean_stringList(input); //Remove matching double tag entries. removeMatches_stringList(input, dt_list); //Reset fixed text box. ui->dt_fixedText->setPlainText(dt_list.join("\n")); return; } void HTML_Checker::on_save_button_clicked() { //Open dialog box, to enter filename and confirm saving. QFileDialog dialog(this); dialog.setWindowModality(Qt::WindowModal); dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.setNameFilter("*.txt"); dialog.setDefaultSuffix("txt"); dialog.selectFile("default"); //Sets default filename. if (dialog.exec() != QDialog::Accepted) return; //Filename is set to first selected file, if multiple were specified. QString filename(dialog.selectedFiles().first()); //Create output text-file. Return error if failed. QFile output(filename); if (!output.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, tr("HTML Checker"), tr("File creation failed.")); return; } //Create output stream and write to file. Tags are separated by ",". //First line are single, second are double tags. QTextStream stream(&output); stream << st_list.join(",") << "\n" << dt_list.join(","); return; } void HTML_Checker::on_load_button_clicked() { //Open dialog box to select file for loading and get filename. QString filename(QFileDialog::getOpenFileName(this)); if (filename.isEmpty()) { return; } //File readable? QFile input(filename); if (!input.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::warning(this, tr("HTML Checker"), tr("File import failed.")); return; } //Open input stream, save content to two lists. QTextStream stream(&input); //1st line should contain single tags. Save them to list and clean. QStringList st_input(stream.readLine().split(",")); clean_stringList(st_input); //2nd line should contain double tags. Save them to list and clean. QStringList dt_input(stream.readLine().split(",")); clean_stringList(dt_input); //Set lists and reset fixed text boxes. st_list = st_input; dt_list = dt_input; ui->st_fixedText->setPlainText(st_list.join("\n")); ui->dt_fixedText->setPlainText(dt_list.join("\n")); return; } void HTML_Checker::on_reset_button_clicked() { init(); return; } void HTML_Checker::on_paste_button_clicked() { //Reset error locations. err_locations.clear(); ui->error_label->setText("Errors Found: "); QPalette black_font; black_font.setColor(QPalette::WindowText, Qt::black); ui->error_label->setPalette(black_font); //Paste clipboard contents to input text box. QClipboard *clipboard = QGuiApplication::clipboard(); ui->input_textEdit->setTextColor(Qt::black); ui->input_textEdit->setPlainText(clipboard->text()); return; } void HTML_Checker::on_check_button_clicked() { //Reset open tags, error counter and text cursor that will be used for highlighting of errors and comments. prev_tags.clear(); exit_flag = false; err_count = 0; err_locations.clear(); flag1 = flag2 = 0; selection = ui->input_textEdit->textCursor(); selection.select(QTextCursor::Document); selection.setCharFormat(neutral_format); selection.movePosition(QTextCursor::Start); //Get content from input box. QString input(ui->input_textEdit->toPlainText()); //First, filter out comments. int index1 = input.indexOf("<!--"); int index2 = input.indexOf("-->"); while (index1 != -1) { //If comment tag isn't closed, increase error counter and highlight the rest of the input. if (index2 == -1) { err_locations += index1; ++err_count; selection.movePosition(QTextCursor::Start); selection.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, index1); while (selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor)) {} selection.setCharFormat(error_format); //Replace the unclosed comment tag with "......" internally and exit loop. input.replace(index1, input.size() - index1 - 1, QString(input.size() - index1 - 1, '.')); index1 = -1; } //Mark comments in blue and replace them with dots internally. else { index2 += 3; //Index2 was index before "-->", now after. input.replace(index1, index2 - index1, QString(index2 - index1, '.')); selection.movePosition(QTextCursor::Start); selection.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, index1); selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, index2 - index1); selection.setCharFormat(comment_format); index1 = input.indexOf("<!--", index2); index2 = input.indexOf("-->", index1); } } //Filter out php directives. index1 = input.indexOf("<?"); index2 = input.indexOf("?>"); while (index1 != -1) { //If php tag isn't closed, increase error counter and highlight the rest of the input. if (index2 == -1) { err_locations += index1; ++err_count; selection.movePosition(QTextCursor::Start); selection.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, index1); while (selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor)) {} selection.setCharFormat(error_format); //Replace the unclosed comment tag with "......" internally and exit loop. input.replace(index1, input.size() - index1 - 1, QString(input.size() - index1 - 1, '.')); index1 = -1; } //Mark php tags in gray and replace them with dots internally. else { index2 += 2; //Index2 was index before "-->", now after. input.replace(index1, index2 - index1, QString(index2 - index1, '.')); selection.movePosition(QTextCursor::Start); selection.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, index1); selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, index2 - index1); selection.setCharFormat(php_format); index1 = input.indexOf("<?", index2); index2 = input.indexOf("?>", index1); } } //Next, search and remove tag attributes (internally), by checking for whitespaces. index1 = input.indexOf('<'); int index3; while (index1 != -1) { index2 = input.indexOf('>', index1); index3 = input.indexOf(' ', index1); //If there's a whitespace between the brackets < >, replace it by > and replace the rest with dots. if ((index3 > index1) && (index3 < index2)) { input.replace(index3, 1, '>'); input.replace(index3 + 1, index2 - index3, QString(index2 - index3, '.')); } index1 = input.indexOf('<', index2); } //Search for single tags and replace them with dots (internally). index1 = input.indexOf('<'); index2 = input.indexOf('>', index1); while ((index1 != -1) && (index2 != -1)) { if (st_list.contains(input.mid(index1, index2 - index1 + 1), Qt::CaseInsensitive)) { input.replace(index1, index2 - index1 + 1, QString(index2 - index1 + 1, '.')); } index1 = input.indexOf('<', index2); index2 = input.indexOf('>', index1); } //Call function to check for faulty double tags. Single tags, attributes and comments have been removed. int position = 0; while (check_string(input, position)) {} //Print number of errors. if (err_count == 0) { ui->error_label->setText("Errors found: 0"); QPalette green_font; green_font.setColor(QPalette::WindowText, Qt::darkGreen); ui->error_label->setPalette(green_font); } else { ui->error_label->setText("Errors found: " + QString::number(err_count)); QPalette red_font; red_font.setColor(QPalette::WindowText, Qt::red); ui->error_label->setPalette(red_font); } return; } bool HTML_Checker::is_close_tag(QString &str) { return (str[1] == '/'); } //Create copy of a tag with closing sign. QString HTML_Checker::convert_to_close_tag(QString str) { return str.insert(1, '/'); } //Function Checks a string for missing close tags after the specified position. //Also marks tags as faulty if they overlap, i.e. <tag1> <tag2> </tag1> </tag2>. //Returns true, until end of string is reached. bool HTML_Checker::check_string(QString &str, int &pos) { int index1 = str.indexOf('<', pos); int index2 = str.indexOf('>', index1); int index3; QString substr1; //If no more tags found, change pos and return false to exit loop. if (index1 == -1) { pos = str.size(); //Makes "str.indexOf" searches for any term after pos return -1. return false; } //If the tag has no closing bracket: Increase error counter, highlight, change pos and return false. if (index2 == -1) { err_locations += index1; ++err_count; selection.movePosition(QTextCursor::Start); selection.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, index1); while (selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor)) {} selection.setCharFormat(error_format); pos = str.size(); return false; } //Else, define substring that contains the tag. //Mark as error if it's a close tag, change pos and return true. substr1 = str.mid(index1, index2 + 1 - index1); if (is_close_tag(substr1)) { //If this is the close tag to a previously opened one, return false and set exit flag. //This exits the while() loop below, which calls the function recursively. if (prev_tags.contains(substr1)) { exit_flag = true; return false; } err_locations += index1; ++err_count; selection.movePosition(QTextCursor::Start); selection.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, index1); selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, index2 - index1 + 1); selection.setCharFormat(error_format); pos = index2; return true; } //If it's a double tag, check next tag. if (dt_list.contains(substr1, Qt::CaseInsensitive)) { index3 = str.indexOf('<', index2); //If no tag found, mark as error and return false. if (index3 == -1) { err_locations += index1; ++err_count; selection.movePosition(QTextCursor::Start); selection.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, index1); selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, index2 - index1 + 1); selection.setCharFormat(error_format); pos = str.size(); return false; } QString substr2 = convert_to_close_tag(substr1); //If next tag is matching closing tag, change pos and return true. if (index3 == str.indexOf(substr2, index2)) { pos = index3 + substr1.size(); return true; } //If it's another tag, call function recursively and try again from new pos. //pos and index3 will be changed in the process. else { pos = index2 + 1; //Add substr2 to the list of searched for close tags. prev_tags.append(substr2); while (index3 != str.indexOf(substr2, pos)) { check_string(str, pos); //If the close tag to a previously opened one was encountered, mark as error. //Also, remove the tag from the close tag list. if (exit_flag == true) { err_locations += index1; ++err_count; selection.movePosition(QTextCursor::Start); selection.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, index1); selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, index2 - index1 + 1); selection.setCharFormat(error_format); exit_flag = false; prev_tags.removeOne(substr2); return false; } index3 = str.indexOf('<', pos); //If no more tags are found, mark error and return false. if (index3 == -1) { err_locations += index1; ++err_count; selection.movePosition(QTextCursor::Start); selection.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, index1); selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, index2 - index1 + 1); selection.setCharFormat(error_format); prev_tags.removeOne(substr2); return false; } } //When matching close tag is found, change pos, remove from list and return true. pos = index3 + substr1.size(); prev_tags.removeOne(substr2); return true; } } //Else, tag is unknown. Increase error count, highlight, change pos and move on. else { err_locations += index1; ++err_count; selection.movePosition(QTextCursor::Start); selection.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, index1); selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, index2 - index1 + 1); selection.setCharFormat(unknown_format); pos = index2 + 1; return true; } } void HTML_Checker::on_jump_button_clicked() { //flag1 marks the previous error number, flag2 the current. if (err_count == 0) { return; } if (flag2 == 0) { selection.movePosition(QTextCursor::Start); selection.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, err_locations[0]); ui->input_textEdit->setTextCursor(selection); ++flag2; } else { selection.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, err_locations[flag2] - err_locations[flag1]); ui->input_textEdit->setTextCursor(selection); ++flag1; ++flag2; } if (flag2 == err_count) { flag2 = flag1 = 0; } }
//Knowledgedump.org - HTML Checker #include <QApplication> #include "html_checker.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); HTML_Checker w; w.show(); return app.exec(); }
Simply paste some HTML code into the text box and push the "Check Input" button. If errors are encountered, the "Jump to Error" button can be used to locate them.
Upon checking the input box, some parts of the content will be color coded. Comments appear grayed out, while php tags are highlighted as blue. If a tag is unknown to the program, it will be magenta colored, while any other error is marked red.
The "Settings" tab can be used to configure the tag list and is mostly self-explanatory. To save changes for later use, the tag lists can be exported to a .txt file. They can either be loaded manually, or automatically on startup, if they are placed in the same directory and named "default.txt". Note that the "Reset to Default" button only changes the list back to its startup state, i.e. the default.txt is reloaded if present – if the lists shall be changed back to their original states, the default.txt file has to be either (re-)moved, or renamed.