feat(chat): markdown formattato nella chat + testo pulito per la voce (v0.5.2)
- src/markdown.{h,cpp}: convertitore minimale, due funzioni:
* toRichText() -> HTML per QML Text.RichText: grassetti (**x**/__x__),
corsivi (*x*/_x_ con confini di parola per non toccare snake_case),
titoli (#), liste (-/1.), citazioni (>), codice (`x` e blocchi ```,
in monospace), link [t](u) -> solo testo, barrato ~~x~~; escape di
< & > contro injection nel rich text
* toSpeech() -> testo piatto per il TTS: via recinti di codice, marcatori
di titoli/liste/citazioni, enfasi, backtick; link/immagini -> testo;
tabelle | a | b | -> "a, b"; righe vuote compattate
- ChatModel: nuovo ruolo contentRich (calcolato in C++, testabile);
incluso nei dataChanged dello streaming
- ChatPage: le bolle dell'assistente usano contentRich con
textFormat: Text.RichText; i messaggi utente restano testo semplice
- ApiClient::speak(): il testo passa da Markdown::toSpeech (niente più
simboli di formattazione letti ad alta voce)
- core-test: 19 casi nuovi per il convertitore (tutti locali, senza device)
Verificato: build zero warning + unit test completi.
This commit is contained in:
@@ -76,7 +76,8 @@ speaks (validated live against a real instance).
|
||||
|
||||
## Status
|
||||
|
||||
Version 0.5.1 — protocol and C++ core validated against a real `hermes-webui`
|
||||
Version 0.5.2 — protocol and C++ core validated against a real `hermes-webui`
|
||||
(login, sessions, streaming chat, TTS, STT, voice dialog, profile selection);
|
||||
UI in English, Italian, French and German; every request has a watchdog
|
||||
timeout (no infinite waits); on-device testing still ongoing.
|
||||
UI in English, Italian, French and German; replies rendered as formatted
|
||||
markdown and cleaned before read-aloud; every request has a watchdog
|
||||
timeout; on-device testing still ongoing.
|
||||
|
||||
+3
-3
@@ -76,12 +76,12 @@ richiede il probe WebUI su :8899). Esito 12/09/2026: unit 9/9, live completo OK.
|
||||
|
||||
```bash
|
||||
cd ~/workspace
|
||||
tar czf harbour-hermes-0.5.1.tar.gz \
|
||||
--transform 's,^harbour-hermes,harbour-hermes-0.5.1,' \
|
||||
tar czf harbour-hermes-0.5.2.tar.gz \
|
||||
--transform 's,^harbour-hermes,harbour-hermes-0.5.2,' \
|
||||
--exclude='.git' harbour-hermes
|
||||
```
|
||||
|
||||
Lo spec si aspetta la directory `harbour-hermes-0.5.1/` (pattern degli altri
|
||||
Lo spec si aspetta la directory `harbour-hermes-0.5.2/` (pattern degli altri
|
||||
progetti: cercato in modo robusto anche per i sorgenti live di sfdk).
|
||||
|
||||
## Traduzioni
|
||||
|
||||
+4
-2
@@ -17,7 +17,8 @@ SOURCES += \
|
||||
src/sessionsmodel.cpp \
|
||||
src/recorder.cpp \
|
||||
src/voicerecorder.cpp \
|
||||
src/player.cpp
|
||||
src/player.cpp \
|
||||
src/markdown.cpp
|
||||
|
||||
HEADERS += \
|
||||
src/settings.h \
|
||||
@@ -28,7 +29,8 @@ HEADERS += \
|
||||
src/sessionsmodel.h \
|
||||
src/recorder.h \
|
||||
src/voicerecorder.h \
|
||||
src/player.h
|
||||
src/player.h \
|
||||
src/markdown.h
|
||||
|
||||
OTHER_FILES += \
|
||||
qml/harbour-hermes.qml \
|
||||
|
||||
@@ -107,7 +107,11 @@ Page {
|
||||
width: Math.min(implicitWidth, bubble.maxW - 2 * Theme.paddingMedium)
|
||||
wrapMode: Text.Wrap
|
||||
color: Theme.primaryColor
|
||||
text: model.content + (model.streaming ? " ▍" : "")
|
||||
// Le risposte di Hermes sono markdown: si mostrano
|
||||
// formattate (grassetti, corsivi, titoli, liste, codice).
|
||||
textFormat: model.role === "user" ? Text.PlainText : Text.RichText
|
||||
text: (model.role === "user" ? model.content : model.contentRich)
|
||||
+ (model.streaming ? " ▍" : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: harbour-hermes
|
||||
Summary: Client for the Hermes Web UI with voice
|
||||
Version: 0.5.1
|
||||
Version: 0.5.2
|
||||
Release: 1
|
||||
Group: Qt/Qt
|
||||
License: MIT
|
||||
@@ -25,6 +25,13 @@ Sessions list, chat with live streaming replies, voice dictation
|
||||
(server-side TTS), plus a hands-free conversation mode.
|
||||
|
||||
%changelog
|
||||
* Sun Sep 13 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.5.2-1
|
||||
- Chat: le risposte di Hermes sono mostrate formattate (grassetti, corsivi,
|
||||
titoli, liste, citazioni, codice, link) con un convertitore markdown ->
|
||||
rich text minimale (src/markdown.*, testato nel core-test); i messaggi
|
||||
dell'utente restano testo semplice
|
||||
- TTS: il markdown viene ripulito prima della sintesi (niente più "asterisco
|
||||
asterisco" letto ad alta voce)
|
||||
* Sun Sep 13 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.5.1-1
|
||||
- Fix blocco sullo splash all'avvio: watchdog con timeout su tutte le
|
||||
richieste (QNAM non ne applica: una richiesta senza risposta lasciava
|
||||
|
||||
+4
-1
@@ -1,4 +1,5 @@
|
||||
#include "apiclient.h"
|
||||
#include "markdown.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
@@ -606,8 +607,10 @@ void ApiClient::speak(const QString &text)
|
||||
const QString trimmed = text.trimmed();
|
||||
if (trimmed.isEmpty())
|
||||
return;
|
||||
QString spoken = Markdown::toSpeech(trimmed);
|
||||
if (spoken.isEmpty())
|
||||
return;
|
||||
// Il server accetta max 5000 caratteri per richiesta: tronca con avviso.
|
||||
QString spoken = trimmed;
|
||||
if (spoken.size() > 4900)
|
||||
spoken = spoken.left(4900);
|
||||
|
||||
|
||||
+6
-1
@@ -1,4 +1,5 @@
|
||||
#include "chatmodel.h"
|
||||
#include "markdown.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
@@ -35,6 +36,9 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
||||
return m.role;
|
||||
case ContentRole:
|
||||
return m.content;
|
||||
case ContentRichRole:
|
||||
// HTML per il rich text QML (markdown formattato lato C++, testabile).
|
||||
return Markdown::toRichText(m.content);
|
||||
case StreamingRole:
|
||||
return m.streaming;
|
||||
case TimestampRole:
|
||||
@@ -49,6 +53,7 @@ QHash<int, QByteArray> ChatModel::roleNames() const
|
||||
QHash<int, QByteArray> roles;
|
||||
roles.insert(RoleRole, "role");
|
||||
roles.insert(ContentRole, "content");
|
||||
roles.insert(ContentRichRole, "contentRich");
|
||||
roles.insert(StreamingRole, "streaming");
|
||||
roles.insert(TimestampRole, "timestamp");
|
||||
return roles;
|
||||
@@ -167,7 +172,7 @@ void ChatModel::onStreamDelta(const QString &text)
|
||||
}
|
||||
last.content += text;
|
||||
const QModelIndex idx = index(m_msgs.size() - 1, 0);
|
||||
emit dataChanged(idx, idx, QVector<int>() << ContentRole << StreamingRole);
|
||||
emit dataChanged(idx, idx, QVector<int>() << ContentRole << ContentRichRole << StreamingRole);
|
||||
emit streamTick();
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ public:
|
||||
enum Roles {
|
||||
RoleRole = Qt::UserRole + 1,
|
||||
ContentRole,
|
||||
ContentRichRole,
|
||||
StreamingRole,
|
||||
TimestampRole
|
||||
};
|
||||
|
||||
+2
-2
@@ -21,9 +21,9 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
// AppConfigLocation = ~/.config/harbour/hermes (zona persistente della sandbox).
|
||||
app->setOrganizationName(QStringLiteral("harbour"));
|
||||
app->setApplicationName(QStringLiteral("hermes"));
|
||||
app->setApplicationVersion(QStringLiteral("0.5.1"));
|
||||
app->setApplicationVersion(QStringLiteral("0.5.2"));
|
||||
|
||||
qDebug() << "harbour-hermes v0.5.1 build" << __DATE__ << __TIME__;
|
||||
qDebug() << "harbour-hermes v0.5.2 build" << __DATE__ << __TIME__;
|
||||
|
||||
// Traduzioni: it/fr/de da /usr/share/harbour-hermes/translations, in base
|
||||
// alla lingua di sistema (l'inglese e' la lingua sorgente: nessun .qm).
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#include "markdown.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QStringList>
|
||||
|
||||
// Nota: il testo arriva dall'agente (fidato ma non nostro), quindi va
|
||||
// escapato per il rich text Qt prima di inserire i NOSTRI tag.
|
||||
// Ordine: escape -> inline (link, grassetto, corsivo) -> blocchi.
|
||||
|
||||
namespace {
|
||||
|
||||
QString escapeHtml(const QString &s)
|
||||
{
|
||||
QString out;
|
||||
out.reserve(s.size());
|
||||
for (int i = 0; i < s.size(); ++i) {
|
||||
const QChar c = s.at(i);
|
||||
if (c == QLatin1Char('&'))
|
||||
out += QLatin1String("&");
|
||||
else if (c == QLatin1Char('<'))
|
||||
out += QLatin1String("<");
|
||||
else if (c == QLatin1Char('>'))
|
||||
out += QLatin1String(">");
|
||||
else
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Inline del rich text (input gia' escapato): codice, link, grassetto,
|
||||
// corsivo. I code span vengono protetti con segnaposto \x01N\x02 per non
|
||||
// subire le altre sostituzioni e ripristinati alla fine in monospace.
|
||||
QString inlineRich(QString s)
|
||||
{
|
||||
QStringList codes;
|
||||
const QRegularExpression codeRe(QStringLiteral("`([^`\n]+)`"));
|
||||
int offset = 0;
|
||||
QRegularExpressionMatch m;
|
||||
while ((m = codeRe.match(s, offset)).hasMatch()) {
|
||||
const QString token = QString(QChar(1)) + QString::number(codes.size())
|
||||
+ QString(QChar(2));
|
||||
codes << QStringLiteral("<font family=\"monospace\">%1</font>")
|
||||
.arg(m.captured(1));
|
||||
s.replace(m.capturedStart(), m.capturedLength(), token);
|
||||
offset = m.capturedStart() + token.size();
|
||||
}
|
||||
|
||||
// [testo](url) -> testo (il markdown non puo' contenere link cliccabili
|
||||
// utili su questo client: meglio il testo pulito).
|
||||
s.replace(QRegularExpression(QStringLiteral("\\[([^\\[\\]]+)\\]\\(([^()\\s]*)\\)")),
|
||||
QStringLiteral("\\1"));
|
||||
// Grassetto **x** / __x__, corsivo *x* / _x_ (con confini di parola per
|
||||
// non toccare gli underscore degli identificatori), barrato ~~x~~.
|
||||
s.replace(QRegularExpression(QStringLiteral("\\*\\*([^*\\n]+)\\*\\*")),
|
||||
QStringLiteral("<b>\\1</b>"));
|
||||
s.replace(QRegularExpression(QStringLiteral("\\b__([^_\\n]+)__\\b")),
|
||||
QStringLiteral("<b>\\1</b>"));
|
||||
s.replace(QRegularExpression(QStringLiteral("(?<!\\*)\\*([^*\\n]+)\\*(?!\\*)")),
|
||||
QStringLiteral("<i>\\1</i>"));
|
||||
s.replace(QRegularExpression(QStringLiteral("\\b_([^_\\n]+)_\\b")),
|
||||
QStringLiteral("<i>\\1</i>"));
|
||||
s.replace(QRegularExpression(QStringLiteral("~~([^~\\n]+)~~")),
|
||||
QStringLiteral("\\1"));
|
||||
|
||||
for (int i = 0; i < codes.size(); ++i) {
|
||||
const QString token = QString(QChar(1)) + QString::number(i)
|
||||
+ QString(QChar(2));
|
||||
s.replace(token, codes.at(i));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
QString toRichTextImpl(const QString &md)
|
||||
{
|
||||
QString text = md;
|
||||
text.replace(QLatin1String("\r\n"), QLatin1String("\n"));
|
||||
text.replace(QLatin1Char('\r'), QLatin1Char('\n'));
|
||||
|
||||
const QStringList lines = text.split(QLatin1Char('\n'));
|
||||
QStringList out;
|
||||
bool inCode = false;
|
||||
|
||||
const QRegularExpression reHeading(QStringLiteral("^\\s{0,3}(#{1,6})\\s+(.*)$"));
|
||||
const QRegularExpression reQuote(QStringLiteral("^\\s*>\\s?(.*)$"));
|
||||
const QRegularExpression reBullet(QStringLiteral("^(\\s*)[-*+]\\s+(.*)$"));
|
||||
const QRegularExpression reOrdered(QStringLiteral("^(\\s*)(\\d+[.)])\\s+(.*)$"));
|
||||
const QRegularExpression reRule(QStringLiteral("^\\s{0,3}([-*_])(\\s*\\1){2,}\\s*$"));
|
||||
const QRegularExpression reTableSep(QStringLiteral("^\\s*\\|?[\\s:|-]+\\|?\\s*$"));
|
||||
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
const QString line = lines.at(i);
|
||||
const QString trimmed = line.trimmed();
|
||||
|
||||
if (trimmed.startsWith(QLatin1String("```"))) {
|
||||
inCode = !inCode;
|
||||
continue; // i recinti non si mostrano
|
||||
}
|
||||
if (inCode) {
|
||||
out << QStringLiteral("<font family=\"monospace\">%1</font>")
|
||||
.arg(escapeHtml(line));
|
||||
continue;
|
||||
}
|
||||
if (trimmed.isEmpty()) {
|
||||
out << QStringLiteral("<br/>");
|
||||
continue;
|
||||
}
|
||||
if (reRule.match(trimmed).hasMatch())
|
||||
continue; // --- separatori: via
|
||||
if (trimmed.contains(QLatin1Char('-')) && trimmed.contains(QLatin1Char('|'))
|
||||
&& reTableSep.match(trimmed).hasMatch())
|
||||
continue; // |---|---| di tabella: via
|
||||
|
||||
QRegularExpressionMatch m = reHeading.match(trimmed);
|
||||
if (m.hasMatch()) {
|
||||
out << QStringLiteral("<b>%1</b>")
|
||||
.arg(inlineRich(escapeHtml(m.captured(2))));
|
||||
continue;
|
||||
}
|
||||
m = reQuote.match(line);
|
||||
if (m.hasMatch()) {
|
||||
out << QStringLiteral("▏ %1").arg(inlineRich(escapeHtml(m.captured(1))));
|
||||
continue;
|
||||
}
|
||||
m = reBullet.match(line);
|
||||
if (m.hasMatch()) {
|
||||
out << QStringLiteral("%1• %2")
|
||||
.arg(m.captured(1), inlineRich(escapeHtml(m.captured(2))));
|
||||
continue;
|
||||
}
|
||||
m = reOrdered.match(line);
|
||||
if (m.hasMatch()) {
|
||||
out << QStringLiteral("%1%2 %3")
|
||||
.arg(m.captured(1), m.captured(2),
|
||||
inlineRich(escapeHtml(m.captured(3))));
|
||||
continue;
|
||||
}
|
||||
out << inlineRich(escapeHtml(line));
|
||||
}
|
||||
|
||||
QString html = out.join(QStringLiteral("<br/>"));
|
||||
while (html.contains(QLatin1String("<br/><br/><br/>")))
|
||||
html.replace(QLatin1String("<br/><br/><br/>"), QLatin1String("<br/><br/>"));
|
||||
while (html.startsWith(QLatin1String("<br/>")))
|
||||
html.remove(0, 5);
|
||||
while (html.endsWith(QLatin1String("<br/>")))
|
||||
html.chop(5);
|
||||
return html;
|
||||
}
|
||||
|
||||
QString toSpeechImpl(const QString &md)
|
||||
{
|
||||
QString text = md;
|
||||
text.replace(QLatin1String("\r\n"), QLatin1String("\n"));
|
||||
text.replace(QLatin1Char('\r'), QLatin1Char('\n'));
|
||||
|
||||
const QStringList lines = text.split(QLatin1Char('\n'));
|
||||
QStringList out;
|
||||
bool inCode = false;
|
||||
|
||||
const QRegularExpression reRule(QStringLiteral("^\\s{0,3}([-*_])(\\s*\\1){2,}\\s*$"));
|
||||
const QRegularExpression reTableSep(QStringLiteral("^\\s*\\|?[\\s:|-]+\\|?\\s*$"));
|
||||
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
QString line = lines.at(i);
|
||||
const QString trimmed = line.trimmed();
|
||||
|
||||
if (trimmed.startsWith(QLatin1String("```"))) {
|
||||
inCode = !inCode;
|
||||
continue;
|
||||
}
|
||||
if (inCode) {
|
||||
out << trimmed; // il contenuto del codice resta
|
||||
continue;
|
||||
}
|
||||
if (reRule.match(trimmed).hasMatch())
|
||||
continue;
|
||||
if (trimmed.contains(QLatin1Char('-')) && trimmed.contains(QLatin1Char('|'))
|
||||
&& reTableSep.match(trimmed).hasMatch())
|
||||
continue;
|
||||
|
||||
line.remove(QRegularExpression(QStringLiteral("^\\s*>\\s?"))); // citazione
|
||||
line.remove(QRegularExpression(QStringLiteral("^\\s{0,3}#{1,6}\\s+"))); // titolo
|
||||
line.remove(QRegularExpression(QStringLiteral("^\\s*[-*+]\\s+"))); // liste
|
||||
// Link e immagini -> solo testo; via backtick e marcatori di enfasi.
|
||||
line.replace(QRegularExpression(QStringLiteral("!?\\[([^\\[\\]]*)\\]\\(([^()\\s]*)\\)")),
|
||||
QStringLiteral("\\1"));
|
||||
line.remove(QLatin1Char('`'));
|
||||
line.remove(QRegularExpression(QStringLiteral("\\*\\*|__|~~")));
|
||||
line.replace(QRegularExpression(QStringLiteral("\\*([^*\\n]+)\\*")),
|
||||
QStringLiteral("\\1"));
|
||||
line.replace(QRegularExpression(QStringLiteral("\\b_([^_\\n]+)_\\b")),
|
||||
QStringLiteral("\\1"));
|
||||
// Righe di tabella "| a | b |" -> "a, b".
|
||||
if (line.count(QLatin1Char('|')) >= 2) {
|
||||
line.remove(QRegularExpression(QStringLiteral("^\\s*\\|")));
|
||||
line.remove(QRegularExpression(QStringLiteral("\\|\\s*$")));
|
||||
line.replace(QRegularExpression(QStringLiteral("\\s*\\|\\s*")),
|
||||
QStringLiteral(", "));
|
||||
}
|
||||
out << line.trimmed();
|
||||
}
|
||||
|
||||
QString plain = out.join(QLatin1Char('\n'));
|
||||
plain.replace(QRegularExpression(QStringLiteral("[ \\t]+")), QStringLiteral(" "));
|
||||
plain.replace(QRegularExpression(QStringLiteral("\\n{3,}")), QStringLiteral("\n\n"));
|
||||
return plain.trimmed();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QString Markdown::toRichText(const QString &md)
|
||||
{
|
||||
return toRichTextImpl(md);
|
||||
}
|
||||
|
||||
QString Markdown::toSpeech(const QString &md)
|
||||
{
|
||||
return toSpeechImpl(md);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef MARKDOWN_H
|
||||
#define MARKDOWN_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
// Conversione minimale del markdown delle risposte di Hermes:
|
||||
// - toRichText(): HTML per QML Text.RichText (grassetti, corsivi, titoli,
|
||||
// liste, citazioni, codice, link) — il sottoinsieme dei rich text Qt;
|
||||
// - toSpeech(): testo piatto per il TTS, senza simboli di formattazione.
|
||||
namespace Markdown {
|
||||
QString toRichText(const QString &md);
|
||||
QString toSpeech(const QString &md);
|
||||
}
|
||||
|
||||
#endif // MARKDOWN_H
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include "apiclient.h"
|
||||
#include "chatmodel.h"
|
||||
#include "markdown.h"
|
||||
#include "settings.h"
|
||||
#include "sseparser.h"
|
||||
|
||||
@@ -78,6 +79,51 @@ static QString readFileTrim(const QString &path)
|
||||
return QString::fromUtf8(f.readAll()).trimmed();
|
||||
}
|
||||
|
||||
static void testMarkdown()
|
||||
{
|
||||
qDebug() << "== unit: markdown ==";
|
||||
|
||||
CHECK(Markdown::toRichText("**ciao**") == QLatin1String("<b>ciao</b>"),
|
||||
"grassetto -> <b>");
|
||||
CHECK(Markdown::toRichText("__ciao__") == QLatin1String("<b>ciao</b>"),
|
||||
"grassetto con underscore -> <b>");
|
||||
CHECK(Markdown::toRichText("*ciao*") == QLatin1String("<i>ciao</i>"),
|
||||
"corsivo -> <i>");
|
||||
CHECK(Markdown::toRichText("# Titolo") == QLatin1String("<b>Titolo</b>"),
|
||||
"titolo -> grassetto");
|
||||
CHECK(Markdown::toRichText("- uno\n- due") == QStringLiteral("• uno<br/>• due"),
|
||||
"lista puntata -> bullet");
|
||||
CHECK(Markdown::toRichText("1. uno\n2. due") == QLatin1String("1. uno<br/>2. due"),
|
||||
"lista numerata conservata");
|
||||
CHECK(Markdown::toRichText("`x = 1`").contains(QLatin1String("monospace")),
|
||||
"codice inline -> monospace");
|
||||
CHECK(Markdown::toRichText("[Hermes](https://x.example)") == QLatin1String("Hermes"),
|
||||
"link -> solo testo");
|
||||
CHECK(Markdown::toRichText("a < b & c") == QLatin1String("a < b & c"),
|
||||
"escape di < e &");
|
||||
CHECK(Markdown::toRichText("2 * 3 = 6") == QLatin1String("2 * 3 = 6"),
|
||||
"asterisco spaiato lasciato intatto");
|
||||
CHECK(Markdown::toRichText("> citazione") == QStringLiteral("▏ citazione"),
|
||||
"citazione con barretta");
|
||||
CHECK(Markdown::toRichText("```\nint x;\n```").contains(QLatin1String("int x;")),
|
||||
"blocco codice: contenuto mostrato");
|
||||
|
||||
CHECK(Markdown::toSpeech("**ciao** *a* `b`") == QLatin1String("ciao a b"),
|
||||
"speech: via enfasi e backtick");
|
||||
CHECK(Markdown::toSpeech("# Titolo\n- uno\n> due") == QLatin1String("Titolo\nuno\ndue"),
|
||||
"speech: titoli/liste/citazioni senza marcatori");
|
||||
CHECK(Markdown::toSpeech("[t](https://x) e ") == QLatin1String("t e img"),
|
||||
"speech: link e immagini -> testo");
|
||||
CHECK(Markdown::toSpeech("snake_case resta") == QLatin1String("snake_case resta"),
|
||||
"speech: underscore in identificatori intatti");
|
||||
CHECK(Markdown::toSpeech("| a | b |\n|---|---|\n| c | d |") == QLatin1String("a, b\nc, d"),
|
||||
"speech: tabella compattata");
|
||||
CHECK(Markdown::toSpeech("```\ncodice()\n```") == QLatin1String("codice()"),
|
||||
"speech: blocco codice senza recinti");
|
||||
CHECK(Markdown::toSpeech("**grassetto**\n\n\n\n*testo*") == QLatin1String("grassetto\n\ntesto"),
|
||||
"speech: righe vuote compattate");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
@@ -85,6 +131,7 @@ int main(int argc, char **argv)
|
||||
app.setApplicationName(QStringLiteral("hermes-test"));
|
||||
|
||||
testParser();
|
||||
testMarkdown();
|
||||
|
||||
if (!app.arguments().contains(QStringLiteral("--live"))) {
|
||||
qDebug() << (failures == 0 ? "UNIT: tutti i test passati" : "UNIT: FALLIMENTI");
|
||||
|
||||
@@ -23,6 +23,7 @@ SOURCES += \
|
||||
$$SRC/recorder.cpp \
|
||||
$$SRC/voicerecorder.cpp \
|
||||
$$SRC/player.cpp \
|
||||
$$SRC/markdown.cpp \
|
||||
core_test.cpp
|
||||
|
||||
HEADERS += \
|
||||
@@ -33,5 +34,6 @@ HEADERS += \
|
||||
$$SRC/chatmodel.h \
|
||||
$$SRC/sessionsmodel.h \
|
||||
$$SRC/recorder.h \
|
||||
$$SRC/markdown.h \
|
||||
$$SRC/voicerecorder.h \
|
||||
$$SRC/player.h
|
||||
|
||||
Reference in New Issue
Block a user