//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;
	}
}
