add markdown parsing + latex + tree-sitter highlighting for codeblocks in llm responses
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
#include "mathtext.hpp"
|
||||
|
||||
#include "latinmodern-fonts.hpp"
|
||||
|
||||
#include <jkqtmathtext/jkqtmathtext.h>
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFontDatabase>
|
||||
|
||||
namespace ZShell::llm {
|
||||
|
||||
namespace {
|
||||
// A few px of breathing room around the equation.
|
||||
constexpr int kRenderMargin = 2;
|
||||
constexpr unsigned int kResolutionDpi = 96;
|
||||
|
||||
// Registers the embedded Latin Modern faces with the font database
|
||||
// (once per process) and reports which families became available.
|
||||
struct LatinModern {
|
||||
bool roman = false;
|
||||
bool math = false;
|
||||
};
|
||||
|
||||
const LatinModern& loadLatinModern() {
|
||||
static const LatinModern fonts = [] {
|
||||
LatinModern result;
|
||||
// addApplicationFont(QByteArray) fails in this environment, so
|
||||
// the embedded bytes are staged to a per-process temp file and
|
||||
// registered through the (stable) file-based API.
|
||||
const auto add = [](const unsigned char* data, size_t size,
|
||||
const QString& fileName, const QString& family) {
|
||||
const QString path = QDir::tempPath() + QLatin1Char('/') + fileName;
|
||||
{
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly) ||
|
||||
f.write(reinterpret_cast<const char*>(data),
|
||||
static_cast<qint64>(size)) != static_cast<qint64>(size))
|
||||
return false;
|
||||
}
|
||||
const int key = QFontDatabase::addApplicationFont(path);
|
||||
if (key < 0)
|
||||
return false;
|
||||
return QFontDatabase::applicationFontFamilies(key).contains(family);
|
||||
};
|
||||
result.roman =
|
||||
add(lmfont::lmroman10_regular, sizeof(lmfont::lmroman10_regular),
|
||||
QStringLiteral("lmroman10-regular.otf"), QStringLiteral("LMRoman10"))
|
||||
&& add(lmfont::lmroman10_italic, sizeof(lmfont::lmroman10_italic),
|
||||
QStringLiteral("lmroman10-italic.otf"), QStringLiteral("LMRoman10"))
|
||||
&& add(lmfont::lmroman10_bold, sizeof(lmfont::lmroman10_bold),
|
||||
QStringLiteral("lmroman10-bold.otf"), QStringLiteral("LMRoman10"))
|
||||
&& add(lmfont::lmroman10_bolditalic, sizeof(lmfont::lmroman10_bolditalic),
|
||||
QStringLiteral("lmroman10-bolditalic.otf"),
|
||||
QStringLiteral("LMRoman10"));
|
||||
result.math = add(
|
||||
lmfont::latinmodern_math, sizeof(lmfont::latinmodern_math),
|
||||
QStringLiteral("latinmodern-math.otf"), QStringLiteral("Latin Modern Math"));
|
||||
return result;
|
||||
}();
|
||||
return fonts;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LlmMathText::LlmMathText(QObject* parent)
|
||||
: QObject(parent), m_renderer(parent, /* useFontsForGUI */ true) {
|
||||
// Latin Modern is the default font of modern LaTeX; use the embedded
|
||||
// faces instead of whatever the system happens to have installed.
|
||||
const LatinModern& fonts = loadLatinModern();
|
||||
if (fonts.roman)
|
||||
m_renderer.setFontRomanAndMath(QStringLiteral("LMRoman10"),
|
||||
JKQTMathTextFontEncoding::MTFEUnicode);
|
||||
if (fonts.math) {
|
||||
// Same pattern as JKQTMathText's useXITS(): the OpenType math
|
||||
// font supplies the math alphabet and operators from its MATH
|
||||
// table.
|
||||
m_renderer.setFontMathRoman(QStringLiteral("Latin Modern Math"),
|
||||
JKQTMathTextFontEncoding::MTFEUnicode);
|
||||
m_renderer.setFallbackFontSymbols(
|
||||
QStringLiteral("Latin Modern Math"),
|
||||
JKQTMathTextFontEncoding::MTFEUnicode);
|
||||
}
|
||||
}
|
||||
|
||||
void LlmMathText::setLatex(const QString& value) {
|
||||
if (m_latex == value)
|
||||
return;
|
||||
m_latex = value;
|
||||
reRender();
|
||||
}
|
||||
|
||||
void LlmMathText::setColor(const QColor& value) {
|
||||
if (m_color == value)
|
||||
return;
|
||||
m_color = value;
|
||||
reRender();
|
||||
}
|
||||
|
||||
void LlmMathText::setFontPointSize(double value) {
|
||||
if (qFuzzyCompare(m_fontPointSize, value))
|
||||
return;
|
||||
m_fontPointSize = value;
|
||||
reRender();
|
||||
}
|
||||
|
||||
void LlmMathText::setDevicePixelRatio(qreal value) {
|
||||
if (qFuzzyCompare(m_devicePixelRatio, value))
|
||||
return;
|
||||
m_devicePixelRatio = value;
|
||||
reRender();
|
||||
}
|
||||
|
||||
void LlmMathText::reRender() {
|
||||
if (m_latex.trimmed().isEmpty()) {
|
||||
m_image = QImage();
|
||||
m_imageUrl = QUrl();
|
||||
m_width = 0;
|
||||
m_height = 0;
|
||||
m_ok = false;
|
||||
Q_EMIT changed();
|
||||
return;
|
||||
}
|
||||
|
||||
m_renderer.setFontPointSize(m_fontPointSize);
|
||||
m_renderer.setFontColor(m_color);
|
||||
|
||||
const bool ok = m_renderer.parse(
|
||||
m_latex,
|
||||
JKQTMathText::LatexParser,
|
||||
JKQTMathText::DefaultParseOptions);
|
||||
if (!ok) {
|
||||
m_image = QImage();
|
||||
m_imageUrl = QUrl();
|
||||
m_width = 0;
|
||||
m_height = 0;
|
||||
m_ok = false;
|
||||
Q_EMIT changed();
|
||||
return;
|
||||
}
|
||||
|
||||
const QImage image = m_renderer.drawIntoImage(
|
||||
/* drawBoxes */ false,
|
||||
QColor(Qt::transparent),
|
||||
kRenderMargin,
|
||||
m_devicePixelRatio,
|
||||
kResolutionDpi);
|
||||
if (image.isNull()) {
|
||||
m_image = QImage();
|
||||
m_imageUrl = QUrl();
|
||||
m_width = 0;
|
||||
m_height = 0;
|
||||
m_ok = false;
|
||||
Q_EMIT changed();
|
||||
return;
|
||||
}
|
||||
|
||||
m_image = image;
|
||||
QByteArray png;
|
||||
{
|
||||
QBuffer buffer(&png);
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
image.save(&buffer, "PNG");
|
||||
}
|
||||
m_imageUrl = QUrl(
|
||||
QStringLiteral("data:image/png;base64,") + QString::fromLatin1(png.toBase64()));
|
||||
// drawIntoImage renders at devicePixelRatio; convert back to
|
||||
// logical pixels.
|
||||
m_width = image.width() / m_devicePixelRatio;
|
||||
m_height = image.height() / m_devicePixelRatio;
|
||||
m_ok = true;
|
||||
Q_EMIT changed();
|
||||
}
|
||||
|
||||
} // namespace ZShell::llm
|
||||
Reference in New Issue
Block a user