sw/Library_sw.mk | 3 sw/source/filter/md/mdcallbcks.cxx | 301 +++++++++++++++ sw/source/filter/md/mdnum.cxx | 52 ++ sw/source/filter/md/mdnum.hxx | 123 ++++++ sw/source/filter/md/mdtab.cxx | 116 ++++++ sw/source/filter/md/swmd.cxx | 711 ++++++++++++++++++++++++++++++++++++- sw/source/filter/md/swmd.hxx | 105 +++++ 7 files changed, 1405 insertions(+), 6 deletions(-)
New commits: commit a58059d76e4398f33f038b78a2838fb3a36641d5 Author: Ujjawal Kumar <randomfores...@gmail.com> AuthorDate: Fri Jul 4 22:51:31 2025 +0530 Commit: Miklos Vajna <vmik...@collabora.com> CommitDate: Tue Aug 26 09:27:38 2025 +0200 Initial implementation of Markdown elements such as paras, lists, and headings Change-Id: I059090be2157141a8cdab44b28054460f648babb Reviewed-on: https://gerrit.libreoffice.org/c/core/+/190199 Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> Reviewed-by: Miklos Vajna <vmik...@collabora.com> diff --git a/sw/Library_sw.mk b/sw/Library_sw.mk index b6dad35ce8e4..1feefd848b65 100644 --- a/sw/Library_sw.mk +++ b/sw/Library_sw.mk @@ -567,6 +567,9 @@ $(eval $(call gb_Library_add_exception_objects,sw,\ sw/source/filter/html/svxcss1 \ sw/source/filter/html/swhtml \ sw/source/filter/html/wrthtml \ + sw/source/filter/md/mdcallbcks \ + sw/source/filter/md/mdnum \ + sw/source/filter/md/mdtab \ sw/source/filter/md/swmd \ sw/source/filter/md/wrtmd \ sw/source/filter/writer/writer \ diff --git a/sw/source/filter/md/mdcallbcks.cxx b/sw/source/filter/md/mdcallbcks.cxx new file mode 100644 index 000000000000..bff393e802ea --- /dev/null +++ b/sw/source/filter/md/mdcallbcks.cxx @@ -0,0 +1,301 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <editeng/postitem.hxx> +#include <IDocumentContentOperations.hxx> + +#include <editeng/crossedoutitem.hxx> +#include <editeng/udlnitem.hxx> +#include <editeng/wghtitem.hxx> +#include <fmtinfmt.hxx> +#include <i18nlangtag/languagetag.hxx> +#include <fmtanchr.hxx> +#include <tools/urlobj.hxx> + +#include "swmd.hxx" + +int SwMarkdownParser::enter_block_callback(MD_BLOCKTYPE type, void* detail, void* userdata) +{ + SwMarkdownParser* parser = static_cast<SwMarkdownParser*>(userdata); + switch (type) + { + case MD_BLOCK_DOC: + break; + case MD_BLOCK_QUOTE: + { + parser->AddBlockQuote(); + break; + } + case MD_BLOCK_UL: + { + parser->StartNumberedBulletList(type); + break; + } + case MD_BLOCK_OL: + { + parser->StartNumberedBulletList(type); + break; + } + case MD_BLOCK_LI: + { + parser->StartNumberedBulletListItem(); + break; + } + case MD_BLOCK_HR: + { + parser->AddHR(); + break; + } + case MD_BLOCK_H: + { + const MD_BLOCK_H_DETAIL* pDetail = static_cast<const MD_BLOCK_H_DETAIL*>(detail); + parser->StartHeading(pDetail->level); + break; + } + case MD_BLOCK_CODE: + parser->BeginCodeBlock(); + break; + case MD_BLOCK_HTML: + parser->BeginHtmlBlock(); + break; + case MD_BLOCK_P: + { + parser->StartPara(); + break; + } + case MD_BLOCK_TABLE: + { + const MD_BLOCK_TABLE_DETAIL* pDetail + = static_cast<const MD_BLOCK_TABLE_DETAIL*>(detail); + const sal_Int32 nRow = pDetail->body_row_count + pDetail->head_row_count; + const sal_Int32 nCol = pDetail->col_count; + parser->StartTable(nRow, nCol); + break; + } + case MD_BLOCK_THEAD: + case MD_BLOCK_TBODY: + break; + case MD_BLOCK_TR: + parser->StartRow(); + break; + case MD_BLOCK_TH: + case MD_BLOCK_TD: + const MD_BLOCK_TD_DETAIL* pDetail = static_cast<const MD_BLOCK_TD_DETAIL*>(detail); + parser->StartCell(pDetail->align); + break; + } + + return 0; +} + +int SwMarkdownParser::leave_block_callback(MD_BLOCKTYPE type, void* /*detail*/, void* userdata) +{ + SwMarkdownParser* parser = static_cast<SwMarkdownParser*>(userdata); + + switch (type) + { + case MD_BLOCK_DOC: + break; + case MD_BLOCK_QUOTE: + parser->EndBlockQuote(); + break; + case MD_BLOCK_UL: + parser->EndNumberedBulletList(); + break; + case MD_BLOCK_OL: + parser->EndNumberedBulletList(); + break; + case MD_BLOCK_LI: + parser->EndNumberedBulletListItem(); + break; + case MD_BLOCK_HR: + { + parser->EndHR(); + break; + } + case MD_BLOCK_H: + { + parser->EndHeading(); + break; + } + case MD_BLOCK_CODE: + parser->EndCodeBlock(); + break; + case MD_BLOCK_HTML: + { + parser->EndHtmlBlock(); + break; + } + case MD_BLOCK_P: + { + parser->EndPara(); + break; + } + case MD_BLOCK_TABLE: + parser->EndTable(); + break; + case MD_BLOCK_THEAD: + case MD_BLOCK_TBODY: + break; + case MD_BLOCK_TR: + case MD_BLOCK_TH: + break; + case MD_BLOCK_TD: + break; + } + return 0; +} + +int SwMarkdownParser::enter_span_callback(MD_SPANTYPE type, void* detail, void* userdata) +{ + SwMarkdownParser* parser = static_cast<SwMarkdownParser*>(userdata); + std::unique_ptr<SfxPoolItem> pItem; + switch (type) + { + case MD_SPAN_EM: + pItem.reset(new SvxPostureItem(ITALIC_NORMAL, RES_CHRATR_POSTURE)); + break; + case MD_SPAN_STRONG: + pItem.reset(new SvxWeightItem(WEIGHT_BOLD, RES_CHRATR_WEIGHT)); + break; + case MD_SPAN_A: + { + const MD_SPAN_A_DETAIL* pHyperlinkDetail = static_cast<const MD_SPAN_A_DETAIL*>(detail); + const MD_ATTRIBUTE& rHref = pHyperlinkDetail->href; + OUString aURL = rtl::OStringToOUString(std::string_view(rHref.text, rHref.size), + RTL_TEXTENCODING_UTF8); + aURL = INetURLObject::GetAbsURL(parser->m_sBaseURL, aURL); + pItem.reset(new SwFormatINetFormat(aURL, OUString())); + break; + } + case MD_SPAN_IMG: + { + const MD_SPAN_IMG_DETAIL* pImgDetail = static_cast<const MD_SPAN_IMG_DETAIL*>(detail); + const MD_ATTRIBUTE& rSrc = pImgDetail->src; + const MD_ATTRIBUTE& rTitle = pImgDetail->title; + OUString aURL = rtl::OStringToOUString(std::string_view(rSrc.text, rSrc.size), + RTL_TEXTENCODING_UTF8); + OUString aTitle = rtl::OStringToOUString(std::string_view(rTitle.text, rTitle.size), + RTL_TEXTENCODING_UTF8); + parser->InsertImage(aURL, aTitle); + parser->m_bInsideImage = true; + break; + } + case MD_SPAN_CODE: + break; + case MD_SPAN_DEL: + pItem.reset(new SvxCrossedOutItem(STRIKEOUT_SINGLE, RES_CHRATR_CROSSEDOUT)); + break; + case MD_SPAN_LATEXMATH: + break; + case MD_SPAN_LATEXMATH_DISPLAY: + break; + case MD_SPAN_WIKILINK: + { + const MD_SPAN_WIKILINK_DETAIL* pWikilink + = static_cast<const MD_SPAN_WIKILINK_DETAIL*>(detail); + OUString target = rtl::OStringToOUString( + std::string_view(pWikilink->target.text, pWikilink->target.size), + RTL_TEXTENCODING_UTF8); + OUString aLang = GetAppLanguageTag().getLanguage(); + OUString aURL = "https://" + aLang + ".wikipedia.org/wiki/" + target; + pItem.reset(new SwFormatINetFormat(aURL, OUString())); + break; + } + case MD_SPAN_U: + pItem.reset(new SvxUnderlineItem(LINESTYLE_SINGLE, RES_CHRATR_CROSSEDOUT)); + break; + } + parser->m_aAttrStack.push_back(std::move(pItem)); + return 0; +} + +int SwMarkdownParser::leave_span_callback(MD_SPANTYPE type, void* /*detail*/, void* userdata) +{ + SwMarkdownParser* parser = static_cast<SwMarkdownParser*>(userdata); + parser->m_aAttrStack.pop_back(); + switch (type) + { + case MD_SPAN_EM: + break; + case MD_SPAN_STRONG: + break; + case MD_SPAN_A: + { + break; + } + case MD_SPAN_IMG: + parser->m_bInsideImage = false; + break; + case MD_SPAN_CODE: + break; + case MD_SPAN_DEL: + break; + case MD_SPAN_LATEXMATH: + break; + case MD_SPAN_LATEXMATH_DISPLAY: + break; + case MD_SPAN_WIKILINK: + break; + case MD_SPAN_U: + break; + } + return 0; +} + +int SwMarkdownParser::text_callback(MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size, + void* userdata) +{ + SwMarkdownParser* parser = static_cast<SwMarkdownParser*>(userdata); + + OUString aText = rtl::OStringToOUString(std::string_view(text, size), RTL_TEXTENCODING_UTF8); + + switch (type) + { + case MD_TEXT_CODE: + if (!parser->m_bInsideImage) + parser->InsertText(aText); + break; + case MD_TEXT_HTML: + parser->m_htmlData += aText; + break; + case MD_TEXT_NORMAL: + if (!parser->m_bInsideImage) + parser->InsertText(aText); + break; + case MD_TEXT_NULLCHAR: + break; + case MD_TEXT_BR: + parser->m_xDoc->getIDocumentContentOperations().InsertString(*parser->m_pPam, + OUString(u' ')); + break; + case MD_TEXT_SOFTBR: + parser->m_xDoc->getIDocumentContentOperations().InsertString(*parser->m_pPam, + OUString(CHAR_HARDBLANK)); + break; + case MD_TEXT_ENTITY: + break; + case MD_TEXT_LATEXMATH: + break; + } + + return 0; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/sw/source/filter/md/mdnum.cxx b/sw/source/filter/md/mdnum.cxx new file mode 100644 index 000000000000..751ddbfccda8 --- /dev/null +++ b/sw/source/filter/md/mdnum.cxx @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <doc.hxx> +#include <ndtxt.hxx> + +#include "mdnum.hxx" + +void SwMdNumRuleInfo::Set(const SwTextNode& rTextNd) +{ + const SwNumRule* pTextNdNumRule(rTextNd.GetNumRule()); + if (pTextNdNumRule && pTextNdNumRule != rTextNd.GetDoc().GetOutlineNumRule()) + { + m_pNumRule = const_cast<SwNumRule*>(pTextNdNumRule); + m_nDeep = o3tl::narrowing<sal_uInt16>(m_pNumRule ? rTextNd.GetActualListLevel() + 1 : 0); + m_bNumbered = rTextNd.IsCountedInList(); + m_bRestart = rTextNd.IsListRestart() && !rTextNd.HasAttrListRestartValue(); + } + else + { + m_pNumRule = nullptr; + m_nDeep = 0; + m_bNumbered = m_bRestart = false; + } +} + +bool SwMdNumRuleInfo::IsRestart(const SwMdNumRuleInfo& rPrev) const +{ + assert(rPrev.GetNumRule() == GetNumRule()); + + if (rPrev.GetDepth() < GetDepth()) + return false; + return m_bRestart; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/sw/source/filter/md/mdnum.hxx b/sw/source/filter/md/mdnum.hxx new file mode 100644 index 000000000000..dda2b6c4b6bb --- /dev/null +++ b/sw/source/filter/md/mdnum.hxx @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <swtypes.hxx> +#include <string.h> + +constexpr tools::Long MD_NUMBER_BULLET_MARGINLEFT = o3tl::toTwips(125, o3tl::Length::mm10); +constexpr tools::Long MD_NUMBER_BULLET_INDENT = -o3tl::toTwips(5, o3tl::Length::mm); + +class SwTextNode; +class SwNumRule; + +class SwMdNumRuleInfo +{ + sal_uInt16 m_aNumStarts[MAXLEVEL]; + SwNumRule* m_pNumRule; // current numbering + sal_uInt16 m_nDeep; // current numbering depth (1, 2, 3, ...) + bool m_bRestart; // Export: restart numbering + bool m_bNumbered; // Export: paragraph is numbered + +public: + SwMdNumRuleInfo() + : m_pNumRule(nullptr) + , m_nDeep(0) + , m_bRestart(false) + , m_bNumbered(false) + { + memset(&m_aNumStarts, 0xff, sizeof(m_aNumStarts)); + } + + SwMdNumRuleInfo(const SwMdNumRuleInfo& rInf) + : m_pNumRule(rInf.m_pNumRule) + , m_nDeep(rInf.m_nDeep) + , m_bRestart(rInf.m_bRestart) + , m_bNumbered(rInf.m_bNumbered) + { + memcpy(&m_aNumStarts, &rInf.m_aNumStarts, sizeof(m_aNumStarts)); + } + + inline void Set(const SwMdNumRuleInfo& rInf); + void Set(const SwTextNode& rTextNd); + + explicit SwMdNumRuleInfo(const SwTextNode& rTextNd) { Set(rTextNd); } + inline SwMdNumRuleInfo& operator=(const SwMdNumRuleInfo& rInf); + + inline void Clear(); + + void SetNumRule(const SwNumRule* pRule) { m_pNumRule = const_cast<SwNumRule*>(pRule); } + SwNumRule* GetNumRule() { return m_pNumRule; } + const SwNumRule* GetNumRule() const { return m_pNumRule; } + + void SetDepth(sal_uInt16 nDepth) { m_nDeep = nDepth; } + sal_uInt16 GetDepth() const { return m_nDeep; } + void IncDepth() { ++m_nDeep; } + void DecDepth() + { + if (m_nDeep != 0) + --m_nDeep; + } + inline sal_uInt8 GetLevel() const; + + bool IsRestart(const SwMdNumRuleInfo& rPrev) const; + + bool IsNumbered() const { return m_bNumbered; } + + inline void SetNodeStartValue(sal_uInt8 nLvl, sal_uInt16 nVal = USHRT_MAX); + sal_uInt16 GetNodeStartValue(sal_uInt8 nLvl) const { return m_aNumStarts[nLvl]; } +}; + +inline SwMdNumRuleInfo& SwMdNumRuleInfo::operator=(const SwMdNumRuleInfo& rInf) +{ + Set(rInf); + return *this; +} + +inline void SwMdNumRuleInfo::Set(const SwMdNumRuleInfo& rInf) +{ + m_pNumRule = rInf.m_pNumRule; + m_nDeep = rInf.m_nDeep; + m_bNumbered = rInf.m_bNumbered; + m_bRestart = rInf.m_bRestart; + memcpy(&m_aNumStarts, &rInf.m_aNumStarts, sizeof(m_aNumStarts)); +} + +inline void SwMdNumRuleInfo::Clear() +{ + m_pNumRule = nullptr; + m_nDeep = 0; + m_bRestart = m_bNumbered = false; + memset(&m_aNumStarts, 0xff, sizeof(m_aNumStarts)); +} + +inline sal_uInt8 SwMdNumRuleInfo::GetLevel() const +{ + return static_cast<sal_uInt8>(m_pNumRule != nullptr && m_nDeep != 0 + ? (m_nDeep <= MAXLEVEL ? m_nDeep - 1 : MAXLEVEL - 1) + : 0); +} + +inline void SwMdNumRuleInfo::SetNodeStartValue(sal_uInt8 nLvl, sal_uInt16 nVal) +{ + m_aNumStarts[nLvl] = nVal; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/sw/source/filter/md/mdtab.cxx b/sw/source/filter/md/mdtab.cxx new file mode 100644 index 000000000000..7dd364348efb --- /dev/null +++ b/sw/source/filter/md/mdtab.cxx @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <swtable.hxx> +#include <itabenum.hxx> +#include <ndtxt.hxx> + +#include "swmd.hxx" + +class MDTable +{ + const SwTable* m_pTable; + SwMarkdownParser* m_pParser; + sal_Int32 m_nRow; + sal_Int32 m_nCol; + sal_Int32 m_nCurRow; + sal_Int32 m_nCurCol; + +public: + MDTable(SwMarkdownParser* pParser) + : m_pParser(pParser) + , m_nCurRow(-1) + , m_nCurCol(-1) + { + m_pParser->RegisterTable(this); + } + + ~MDTable() { m_pParser->DeRegisterTable(this); } + + const SwTable* GetTable() { return m_pTable; } + sal_Int32 GetCurRow() { return m_nCurRow; } + sal_Int32 GetCurCol() { return m_nCurCol; } + + void SetTable(const SwTable* pTable, sal_Int32 nRow, sal_Int32 nCol); + inline void IncCurRow(); + inline void IncCurCol(); +}; + +void MDTable::SetTable(const SwTable* pTable, sal_Int32 nRow, sal_Int32 nCol) +{ + m_pTable = pTable; + m_nRow = nRow; + m_nCol = nCol; +} + +void SwMarkdownParser::StartTable(sal_Int32 nRow, sal_Int32 nCol) +{ + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_SPACE); + else + AddParSpace(); + + std::shared_ptr<MDTable> xTable = std::make_shared<MDTable>(this); + m_xTable = xTable; + + const SwTable* pTable + = m_xDoc->InsertTable(SwInsertTableOptions(SwInsertTableFlags::All, 1), *m_pPam->GetPoint(), + nRow, nCol, text::HoriOrientation::FULL); + m_xTable->SetTable(pTable, nRow, nCol); +} + +void SwMarkdownParser::EndTable() +{ + m_pPam->Move(fnMoveForward); + AppendTextNode(AM_SPACE); + m_xTable.reset(); +} + +void SwMarkdownParser::StartRow() { m_xTable->IncCurRow(); } + +void SwMarkdownParser::StartCell(MD_ALIGN eAdjust) +{ + m_xTable->IncCurCol(); + SwTableBox* pBox = m_xTable->GetTable() + ->GetTabLines()[m_xTable->GetCurRow()] + ->GetTabBoxes()[m_xTable->GetCurCol()]; + const SwStartNode* pSttNd = pBox->GetSttNd(); + SwTextNode* pTextNode + = pSttNd->GetNodes()[pSttNd->GetIndex() + 1]->GetContentNode()->GetTextNode(); + + if (!pTextNode) + { + return; + } + + SvxAdjustItem aAdjustItem(adjustMap.at(eAdjust), RES_PARATR_ADJUST); + pTextNode->SetAttr(aAdjustItem); + + m_pPam->GetPoint()->Assign(*pTextNode, 0); +} + +inline void MDTable::IncCurRow() +{ + m_nCurRow++; + m_nCurCol = -1; +} + +inline void MDTable::IncCurCol() { m_nCurCol++; } + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/sw/source/filter/md/swmd.cxx b/sw/source/filter/md/swmd.cxx index 626e1e24b60f..63c61bf3c3ae 100644 --- a/sw/source/filter/md/swmd.cxx +++ b/sw/source/filter/md/swmd.cxx @@ -17,15 +17,665 @@ * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ +#include <osl/diagnose.h> +#include <list.hxx> +#include <numrule.hxx> +#include <node.hxx> +#include <ndtxt.hxx> +#include <fmthdft.hxx> #include <fltini.hxx> +#include <itabenum.hxx> +#include <fchrfmt.hxx> +#include <swerror.h> +#include <strings.hrc> +#include <mdiexp.hxx> +#include <poolfmt.hxx> #include <iodetect.hxx> +#include <hintids.hxx> +#include <sfx2/docfile.hxx> +#include <sfx2/sfxsids.hrc> +#include <svl/itemiter.hxx> +#include <IDocumentStylePoolAccess.hxx> +#include <fmtinfmt.hxx> +#include <frmatr.hxx> +#include <fmtanchr.hxx> +#include <fmtfsize.hxx> +#include <unotools/securityoptions.hxx> +#include <vcl/graph.hxx> +#include <vcl/graphicfilter.hxx> +#include <ndgrf.hxx> +#include <fmtcntnt.hxx> +#include <swtypes.hxx> #include "swmd.hxx" -SwMarkdownParser::SwMarkdownParser(SwDoc& rD, SwPaM& rCursor, SvStream& rIn, bool bReadNewDoc) +namespace +{ +bool allowAccessLink(const SwDoc& rDoc) +{ + OUString sReferer; + SfxObjectShell* sh = rDoc.GetPersist(); + if (sh != nullptr && sh->HasName()) + { + sReferer = sh->GetMedium()->GetName(); + } + return !SvtSecurityOptions::isUntrustedReferer(sReferer); +} +} + +void SwMarkdownParser::SetNodeNum(sal_uInt8 nLevel) +{ + SwTextNode* pTextNode = m_pPam->GetPointNode().GetTextNode(); + if (!pTextNode) + { + SAL_WARN("sw.md", "No Text-Node at PaM-Position"); + return; + } + + OSL_ENSURE(GetNumInfo().GetNumRule(), "No numbering rule"); + const OUString& rName = GetNumInfo().GetNumRule()->GetName(); + static_cast<SwContentNode*>(pTextNode)->SetAttr(SwNumRuleItem(rName)); + + pTextNode->SetAttrListLevel(nLevel); + pTextNode->SetCountedInList(false); + + // Invalidate NumRule, it may have been set valid because of an EndAction + GetNumInfo().GetNumRule()->Invalidate(); +} + +sal_Int32 SwMarkdownParser::StripTrailingLF() +{ + sal_Int32 nStripped = 0; + + const sal_Int32 nLen = m_pPam->GetPoint()->GetContentIndex(); + if (nLen) + { + SwTextNode* pTextNd = m_pPam->GetPoint()->GetNode().GetTextNode(); + + if (pTextNd) + { + sal_Int32 nPos = nLen; + sal_Int32 nLFCount = 0; + while (nPos && ('\x0a' == pTextNd->GetText()[--nPos])) + nLFCount++; + + if (nLFCount) + { + if (nLFCount > 2) + { + nLFCount = 2; + } + + nPos = nLen - nLFCount; + SwContentIndex nIdx(pTextNd, nPos); + pTextNd->EraseText(nIdx, nLFCount); + nStripped = nLFCount; + } + } + } + + return nStripped; +} + +bool SwMarkdownParser::AppendTextNode(SwMdAppendMode eMode, bool bUpdateNum) +{ + sal_Int32 nLFStripped = StripTrailingLF(); + if ((AM_NOSPACE == eMode || AM_SOFTNOSPACE == eMode) && nLFStripped > 1) + eMode = AM_SPACE; + + SwTextNode* pTextNode = (AM_SPACE == eMode || AM_NOSPACE == eMode) + ? m_pPam->GetPoint()->GetNode().GetTextNode() + : nullptr; + + if (pTextNode) + { + const SvxULSpaceItem& rULSpace = pTextNode->SwContentNode::GetAttr(RES_UL_SPACE); + + bool bChange = AM_NOSPACE == eMode ? rULSpace.GetLower() > 0 : rULSpace.GetLower() == 0; + + if (bChange) + { + const SvxULSpaceItem& rCollULSpace = pTextNode->GetAnyFormatColl().GetULSpace(); + + bool bMayReset + = AM_NOSPACE == eMode ? rCollULSpace.GetLower() == 0 : rCollULSpace.GetLower() > 0; + + if (bMayReset && rCollULSpace.GetUpper() == rULSpace.GetUpper()) + { + pTextNode->ResetAttr(RES_UL_SPACE); + } + else + { + pTextNode->SetAttr(SvxULSpaceItem( + rULSpace.GetUpper(), AM_NOSPACE == eMode ? 0 : MD_PARSPACE, RES_UL_SPACE)); + } + } + } + m_bNoParSpace = AM_NOSPACE == eMode || AM_SOFTNOSPACE == eMode; + + bool bRet = m_xDoc->getIDocumentContentOperations().AppendTextNode(*m_pPam->GetPoint()); + + if (bUpdateNum) + { + if (GetNumInfo().GetDepth()) + { + sal_uInt8 nLvl = GetNumInfo().GetLevel(); + SetNodeNum(nLvl); + } + else + m_pPam->GetPointNode().GetTextNode()->ResetAttr(RES_PARATR_NUMRULE); + } + + return bRet; +} + +void SwMarkdownParser::AddParSpace() +{ + //If it already has ParSpace, return + if (!m_bNoParSpace) + return; + + m_bNoParSpace = false; + + SwNodeOffset nNdIdx = m_pPam->GetPoint()->GetNodeIndex() - 1; + + SwTextNode* pTextNode = m_xDoc->GetNodes()[nNdIdx]->GetTextNode(); + if (!pTextNode) + return; + + SvxULSpaceItem rULSpace = pTextNode->SwContentNode::GetAttr(RES_UL_SPACE); + if (rULSpace.GetLower()) + return; + + const SvxULSpaceItem& rCollULSpace = pTextNode->GetAnyFormatColl().GetULSpace(); + if (rCollULSpace.GetLower() && rCollULSpace.GetUpper() == rULSpace.GetUpper()) + { + pTextNode->ResetAttr(RES_UL_SPACE); + } + + pTextNode->SetAttr(SvxULSpaceItem(rULSpace.GetUpper(), MD_PARSPACE, RES_UL_SPACE)); +} + +void SwMarkdownParser::AddBlockQuote() +{ + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_SPACE); + else + AddParSpace(); + + SwTextFormatColl* pColl + = m_xDoc->getIDocumentStylePoolAccess().GetTextCollFromPool(RES_POOLCOLL_HTML_BLOCKQUOTE); + + m_nBlockQuoteDepth++; + + sal_Int32 nBaseLeftIndent = pColl->GetTextLeftMargin().ResolveTextLeft({}); + sal_Int32 nLeftIndent = nBaseLeftIndent + m_nBlockQuoteDepth * nBaseLeftIndent; + SvxTextLeftMarginItem aLeftMargin(pColl->GetTextLeftMargin()); + aLeftMargin.SetTextLeft(SvxIndentValue::twips(nLeftIndent)); + m_xDoc->getIDocumentContentOperations().InsertPoolItem(*m_pPam, aLeftMargin); +} + +void SwMarkdownParser::EndBlockQuote() +{ + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_SPACE); + else + AddParSpace(); + + if (m_nBlockQuoteDepth == 0) + { + SwTextFormatColl* pColl + = m_xDoc->getIDocumentStylePoolAccess().GetTextCollFromPool(RES_POOLCOLL_TEXT); + m_xDoc->SetTextFormatColl(*m_pPam, pColl); + } + + m_nBlockQuoteDepth--; +} + +void SwMarkdownParser::AddHR() +{ + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_SPACE); + + SwTextFormatColl* pColl + = m_xDoc->getIDocumentStylePoolAccess().GetTextCollFromPool(RES_POOLCOLL_HTML_HR); + m_xDoc->SetTextFormatColl(*m_pPam, pColl); +} + +void SwMarkdownParser::EndHR() +{ + AppendTextNode(AM_SPACE); + SwTextFormatColl* pColl + = m_xDoc->getIDocumentStylePoolAccess().GetTextCollFromPool(RES_POOLCOLL_TEXT); + m_xDoc->SetTextFormatColl(*m_pPam, pColl); +} + +void SwMarkdownParser::StartPara() +{ + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_SPACE); + else + AddParSpace(); +} + +void SwMarkdownParser::EndPara() +{ + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_SPACE); + else + AddParSpace(); +} + +void SwMarkdownParser::StartHeading(sal_uInt8 nLvl) +{ + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_SPACE); + else + AddParSpace(); + + SwTextFormatColl* pColl = m_xDoc->getIDocumentStylePoolAccess().GetTextCollFromPool( + RES_POOLCOLL_HEADLINE_BASE + nLvl); + m_xDoc->SetTextFormatColl(*m_pPam, pColl); +} + +void SwMarkdownParser::EndHeading() +{ + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_SPACE); + else + AddParSpace(); + + SwTextFormatColl* pColl + = m_xDoc->getIDocumentStylePoolAccess().GetTextCollFromPool(RES_POOLCOLL_TEXT); + m_xDoc->SetTextFormatColl(*m_pPam, pColl); +} + +void SwMarkdownParser::StartNumberedBulletList(MD_BLOCKTYPE aListType) +{ + SwMdNumRuleInfo& rInfo = GetNumInfo(); + + bool bSpace = (rInfo.GetDepth()) == 0; + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(bSpace ? AM_SPACE : AM_NOSPACE, false); + else if (bSpace) + AddParSpace(); + + rInfo.IncDepth(); + sal_uInt8 nLevel + = static_cast<sal_uInt8>((rInfo.GetDepth() <= MAXLEVEL ? rInfo.GetDepth() : MAXLEVEL) - 1); + + if (!rInfo.GetNumRule()) + { + sal_uInt16 nPos = m_xDoc->MakeNumRule(m_xDoc->GetUniqueNumRuleName()); + rInfo.SetNumRule(m_xDoc->GetNumRuleTable()[nPos]); + } + + bool bNewNumFormat = rInfo.GetNumRule()->GetNumFormat(nLevel) == nullptr; + bool bChangeNumFormat = false; + + // Create the default numbering format + SwNumFormat aNumFormat(rInfo.GetNumRule()->Get(nLevel)); + rInfo.SetNodeStartValue(nLevel); + if (bNewNumFormat) + { + sal_uInt16 nChrFormatPoolId = 0; + if (aListType == MD_BLOCK_OL) + { + aNumFormat.SetNumberingType(SVX_NUM_ARABIC); + nChrFormatPoolId = RES_POOLCHR_NUM_LEVEL; + } + else + { + if (numfunc::IsDefBulletFontUserDefined()) + { + aNumFormat.SetBulletFont(&numfunc::GetDefBulletFont()); + } + aNumFormat.SetNumberingType(SVX_NUM_CHAR_SPECIAL); + aNumFormat.SetBulletChar(cBulletChar); + nChrFormatPoolId = RES_POOLCHR_BULLET_LEVEL; + } + + sal_Int32 nAbsLSpace = MD_NUMBER_BULLET_MARGINLEFT; + + sal_Int32 nFirstLineIndent = MD_NUMBER_BULLET_INDENT; + if (nLevel > 0) + { + const SwNumFormat& rPrevNumFormat = rInfo.GetNumRule()->Get(nLevel - 1); + nAbsLSpace = nAbsLSpace + rPrevNumFormat.GetAbsLSpace(); + nFirstLineIndent = rPrevNumFormat.GetFirstLineOffset(); + } + aNumFormat.SetAbsLSpace(nAbsLSpace); + aNumFormat.SetFirstLineOffset(nFirstLineIndent); + aNumFormat.SetCharFormat( + m_xDoc->getIDocumentStylePoolAccess().GetCharFormatFromPool(nChrFormatPoolId)); + + bChangeNumFormat = true; + } + else if (1 != aNumFormat.GetStart()) + { + // If the layer has already been used, the start value may need to be set hard to the paragraph. + rInfo.SetNodeStartValue(nLevel, 1); + } + + { + sal_uInt8 nLvl = nLevel; + SetNodeNum(nLvl); + } + + if (bChangeNumFormat) + { + rInfo.GetNumRule()->Set(nLevel, aNumFormat); + m_xDoc->ChgNumRuleFormats(*rInfo.GetNumRule()); + } +} + +void SwMarkdownParser::EndNumberedBulletList() +{ + SwMdNumRuleInfo& rInfo = GetNumInfo(); + + // A new paragraph needs to be created, when + // - the current one isn't empty (it contains text or paragraph-bound objects) + // - the current one is numbered + bool bAppend = m_pPam->GetPoint()->GetContentIndex() > 0; + if (!bAppend) + { + SwTextNode* pTextNode = m_pPam->GetPointNode().GetTextNode(); + + bAppend = (pTextNode && !pTextNode->IsOutline() && pTextNode->IsCountedInList()); + } + + bool bSpace = (rInfo.GetDepth()) == 1; + if (bAppend) + AppendTextNode(bSpace ? AM_SPACE : AM_NOSPACE, false); + else if (bSpace) + AddParSpace(); + + if (rInfo.GetDepth() > 0) + { + rInfo.DecDepth(); + if (!rInfo.GetDepth()) + { + // The formats not yet modified are now modified, to ease editing + const SwNumFormat* pRefNumFormat = nullptr; + bool bChanged = false; + for (sal_uInt16 i = 0; i < MAXLEVEL; i++) + { + const SwNumFormat* pNumFormat = rInfo.GetNumRule()->GetNumFormat(i); + if (pNumFormat) + { + pRefNumFormat = pNumFormat; + } + else if (pRefNumFormat) + { + SwNumFormat aNumFormat(rInfo.GetNumRule()->Get(i)); + aNumFormat.SetNumberingType(pRefNumFormat->GetNumberingType() != SVX_NUM_BITMAP + ? pRefNumFormat->GetNumberingType() + : SVX_NUM_CHAR_SPECIAL); + if (SVX_NUM_CHAR_SPECIAL == aNumFormat.GetNumberingType()) + { + if (numfunc::IsDefBulletFontUserDefined()) + { + aNumFormat.SetBulletFont(&numfunc::GetDefBulletFont()); + } + aNumFormat.SetBulletChar(cBulletChar); + } + aNumFormat.SetAbsLSpace((i + 1) * MD_NUMBER_BULLET_MARGINLEFT); + aNumFormat.SetFirstLineOffset(MD_NUMBER_BULLET_INDENT); + aNumFormat.SetCharFormat(pRefNumFormat->GetCharFormat()); + rInfo.GetNumRule()->Set(i, aNumFormat); + bChanged = true; + } + } + if (bChanged) + m_xDoc->ChgNumRuleFormats(*rInfo.GetNumRule()); + + // On the last append, the NumRule item and NodeNum object were copied. + // Now we need to delete them. ResetAttr deletes the NodeNum object as well + if (SwTextNode* pTextNode = m_pPam->GetPointNode().GetTextNode()) + pTextNode->ResetAttr(RES_PARATR_NUMRULE); + + rInfo.Clear(); + } + else + { + // the next paragraph not numbered first + SetNodeNum(rInfo.GetLevel()); + } + } +} + +void SwMarkdownParser::StartNumberedBulletListItem() +{ + sal_uInt8 nLevel = GetNumInfo().GetLevel(); + sal_uInt16 nStart = GetNumInfo().GetNodeStartValue(nLevel); + + GetNumInfo().SetNodeStartValue(nLevel); + + // create a new paragraph + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_NOSPACE, false); + m_bNoParSpace = false; // no space in <LI>! + + SwTextNode* pTextNode = m_pPam->GetPointNode().GetTextNode(); + if (!pTextNode) + { + SAL_WARN("sw.md", "No Text-Node at PaM-Position"); + return; + } + + OUString aNumRuleName; + if (GetNumInfo().GetNumRule()) + { + aNumRuleName = GetNumInfo().GetNumRule()->GetName(); + } + else + { + aNumRuleName = m_xDoc->GetUniqueNumRuleName(); + SwNumRule aNumRule(aNumRuleName, SvxNumberFormat::LABEL_WIDTH_AND_POSITION); + SwNumFormat aNumFormat(aNumRule.Get(0)); + if (numfunc::IsDefBulletFontUserDefined()) + { + aNumFormat.SetBulletFont(&numfunc::GetDefBulletFont()); + } + aNumFormat.SetNumberingType(SVX_NUM_CHAR_SPECIAL); + aNumFormat.SetBulletChar(cBulletChar); + aNumFormat.SetCharFormat( + m_xDoc->getIDocumentStylePoolAccess().GetCharFormatFromPool(RES_POOLCHR_BULLET_LEVEL)); + aNumFormat.SetFirstLineOffset(MD_NUMBER_BULLET_INDENT); + aNumRule.Set(0, aNumFormat); + + m_xDoc->MakeNumRule(aNumRuleName, &aNumRule); + } + + static_cast<SwContentNode*>(pTextNode)->SetAttr(SwNumRuleItem(aNumRuleName)); + pTextNode->SetAttrListLevel(nLevel); + + if (nLevel < MAXLEVEL) + { + pTextNode->SetCountedInList(true); + } + + if (nStart != USHRT_MAX) + { + pTextNode->SetListRestart(true); + pTextNode->SetAttrListRestartValue(nStart); + } + + if (GetNumInfo().GetNumRule()) + GetNumInfo().GetNumRule()->Invalidate(); +} + +void SwMarkdownParser::EndNumberedBulletListItem() +{ + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_SPACE); +} + +void SwMarkdownParser::BeginHtmlBlock() +{ + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_SPACE); + else + AddParSpace(); +} + +void SwMarkdownParser::InsertHtmlData() +{ + OString aData = rtl::OUStringToOString(m_htmlData, RTL_TEXTENCODING_UTF8); + SvMemoryStream aStream(const_cast<char*>(aData.getStr()), aData.getLength(), StreamMode::READ); + SwReader aReader(aStream, OUString(), OUString(), *m_pPam); + aReader.Read(*ReadHTML); +} + +void SwMarkdownParser::EndHtmlBlock() +{ + InsertHtmlData(); + m_htmlData.clear(); +} + +void SwMarkdownParser::BeginCodeBlock() +{ + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_SPACE); + else + AddParSpace(); + + SwTextFormatColl* pColl + = m_xDoc->getIDocumentStylePoolAccess().GetTextCollFromPool(RES_POOLCOLL_HTML_PRE); + m_xDoc->SetTextFormatColl(*m_pPam, pColl); + + SvxBrushItem aBrushItem(COL_CODE_BLOCK, RES_BACKGROUND); + m_xDoc->getIDocumentContentOperations().InsertPoolItem(*m_pPam, aBrushItem); +} + +void SwMarkdownParser::EndCodeBlock() +{ + if (m_pPam->GetPoint()->GetContentIndex()) + AppendTextNode(AM_SPACE); + else + AddParSpace(); + + SwTextFormatColl* pColl + = m_xDoc->getIDocumentStylePoolAccess().GetTextCollFromPool(RES_POOLCOLL_TEXT); + m_xDoc->SetTextFormatColl(*m_pPam, pColl); + ClearAttrs(); +} + +void SwMarkdownParser::InsertText(OUString& aStr) +{ + SwContentNode* pCnd = m_pPam->GetPointContentNode(); + sal_Int32 nStartPos = m_pPam->GetPoint()->GetContentIndex(); + + m_xDoc->getIDocumentContentOperations().InsertString(*m_pPam, aStr); + + SwPaM aAttrPam(*m_pPam->GetPoint()); + aAttrPam.SetMark(); + aAttrPam.GetMark()->Assign(*pCnd, nStartPos); + + if (!m_aAttrStack.empty()) + { + SetAttrs(aAttrPam); + ClearAttrs(); + } +} + +void SwMarkdownParser::SetAttrs(SwPaM& rRange) +{ + for (const auto& pItem : m_aAttrStack) + { + if (pItem) + { + if (rRange.HasMark() && (*rRange.GetMark() != *rRange.GetPoint())) + { + m_xDoc->getIDocumentContentOperations().InsertPoolItem(rRange, *pItem); + } + } + } +} + +void SwMarkdownParser::ClearAttrs() { m_xDoc->ResetAttrs(*m_pPam, true); } + +void SwMarkdownParser::InsertImage(const OUString& aURL, const OUString& rTitle) +{ + OUString sGrfNm = INetURLObject::GetAbsURL(m_sBaseURL, aURL); + + Graphic aGraphic; + INetURLObject aGraphicURL(sGrfNm); + + if (!sGrfNm.isEmpty()) + { + aGraphic.SetDefaultType(); + } + + Size aGrfSz(0, 0); + if (allowAccessLink(*m_xDoc) && !aGraphicURL.IsExoticProtocol()) + { + GraphicDescriptor aDescriptor(aGraphicURL); + if (aDescriptor.Detect(true)) + aGrfSz + = o3tl::convert(aDescriptor.GetSizePixel(), o3tl::Length::px, o3tl::Length::twip); + } + + tools::Long nWidth = aGrfSz.getWidth(); + tools::Long nHeight = aGrfSz.getHeight(); + + if (nWidth > 0 && nHeight > 0) + { + if (nWidth > MD_MAX_IMAGE_WIDTH_IN_TWIPS || nHeight > MD_MAX_IMAGE_HEIGH_IN_TWIPS) + { + double fScaleX = static_cast<double>(MD_MAX_IMAGE_WIDTH_IN_TWIPS) / nWidth; + double fScaleY = static_cast<double>(MD_MAX_IMAGE_HEIGH_IN_TWIPS) / nHeight; + double fScale = std::min(fScaleX, fScaleY); + + nWidth = static_cast<tools::Long>(nWidth * fScale); + nHeight = static_cast<tools::Long>(nHeight * fScale); + } + } + + if (nWidth < MINFLY || nHeight < MINFLY) + { + nWidth = MINFLY; + nHeight = MINFLY; + } + + SfxItemSet aFlySet( + SfxItemSet::makeFixedSfxItemSet<RES_FRM_SIZE, RES_VERT_ORIENT, RES_HORI_ORIENT, RES_ANCHOR>( + m_xDoc->GetAttrPool())); + + aFlySet.Put(SwFormatAnchor(RndStdIds::FLY_AS_CHAR)); + aFlySet.Put(SwFormatFrameSize(SwFrameSize::Fixed, nWidth, nHeight)); + aFlySet.Put(SwFormatHoriOrient(0, text::HoriOrientation::NONE, text::RelOrientation::CHAR)); + aFlySet.Put( + SwFormatVertOrient(0, text::VertOrientation::CHAR_CENTER, text::RelOrientation::CHAR)); + + SanitizeAnchor(aFlySet); + + SwFlyFrameFormat* pFlyFormat = m_xDoc->getIDocumentContentOperations().InsertGraphic( + *m_pPam, sGrfNm, OUString(), &aGraphic, &aFlySet, nullptr, nullptr); + + SwGrfNode* pGrfNd = m_xDoc->GetNodes()[pFlyFormat->GetContent().GetContentIdx()->GetIndex() + 1] + ->GetGrfNode(); + + if (pGrfNd && !rTitle.isEmpty()) + { + pGrfNd->SetTitle(rTitle); + } + + m_bNoParSpace = true; +} + +void SwMarkdownParser::RegisterTable(MDTable* pTable) { m_aTables.push_back(pTable); } + +void SwMarkdownParser::DeRegisterTable(MDTable* pTable) { std::erase(m_aTables, pTable); } + +SwMarkdownParser::SwMarkdownParser(SwDoc& rD, SwPaM& rCursor, SvStream& rIn, OUString aBaseURL, + bool bReadNewDoc) : m_xDoc(&rD) , m_rInput(rIn) // , m_pMedium(&rMedium) + , m_pNumRuleInfo(new SwMdNumRuleInfo) + , m_sBaseURL(std::move(aBaseURL)) + , m_nBlockQuoteDepth(-1) , m_bNewDoc(bReadNewDoc) { rCursor.DeleteMark(); @@ -34,18 +684,71 @@ SwMarkdownParser::SwMarkdownParser(SwDoc& rD, SwPaM& rCursor, SvStream& rIn, boo m_nFilesize = m_rInput.TellEnd(); m_rInput.Seek(STREAM_SEEK_TO_BEGIN); m_rInput.ResetError(); - m_pArr.reset(new char[m_nFilesize + 2]); + m_pArr.reset(new char[m_nFilesize + 1]); } -ErrCodeMsg MarkdownReader::Read(SwDoc&, const OUString&, SwPaM&, const OUString&) +ErrCodeMsg MarkdownReader::Read(SwDoc& rDoc, const OUString& rBaseURL, SwPaM& rPam, const OUString&) { - return ERRCODE_NONE; + ErrCode nRet; + + SwMarkdownParser parser(rDoc, rPam, *m_pStream, rBaseURL, !m_bInsertMode); + nRet = parser.CallParser(); + + return nRet; +} + +ErrCode SwMarkdownParser::CallParser() +{ + ::StartProgress(STR_STATSTR_W4WREAD, 0, m_nFilesize, m_xDoc->GetDocShell()); + + SwTextFormatColl* pColl + = m_xDoc->getIDocumentStylePoolAccess().GetTextCollFromPool(RES_POOLCOLL_TEXT); + m_xDoc->SetTextFormatColl(*m_pPam, pColl); + m_rInput.ReadBytes(m_pArr.get(), m_nFilesize); + m_pArr[m_nFilesize] = ' + + ErrCode nRet; + + MD_PARSER parser = { 0, + MD_DIALECT_GITHUB | MD_FLAG_WIKILINKS, + enter_block_callback, + leave_block_callback, + enter_span_callback, + leave_span_callback, + text_callback, + nullptr, + nullptr }; + + int result = md_parse(m_pArr.get(), m_nFilesize, &parser, static_cast<void*>(this)); + + if (result != 0) + { + nRet = ERRCODE_IO_GENERAL; + } + + ::EndProgress(m_xDoc->GetDocShell()); + return nRet; } SwMarkdownParser::~SwMarkdownParser() { m_pArr.reset(); + m_pNumRuleInfo.reset(); m_xDoc.clear(); } +//static +void SwMarkdownParser::SanitizeAnchor(SfxItemSet& rFrameItemSet) +{ + const SwFormatAnchor& rAnch = rFrameItemSet.Get(RES_ANCHOR); + if (SwNode* pAnchorNode = rAnch.GetAnchorNode()) + { + if (pAnchorNode->IsEndNode()) + { + SAL_WARN("sw.md", "Invalid EndNode Anchor"); + rFrameItemSet.ClearItem(RES_ANCHOR); + } + } +} + /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/sw/source/filter/md/swmd.hxx b/sw/source/filter/md/swmd.hxx index dbdd2c6e3167..795b6d0d902d 100644 --- a/sw/source/filter/md/swmd.hxx +++ b/sw/source/filter/md/swmd.hxx @@ -21,11 +21,40 @@ #include <memory> +#include <tools/stream.hxx> #include <doc.hxx> #include <pam.hxx> + #include <md4c.h> #include <o3tl/unit_conversion.hxx> -#include <tools/stream.hxx> +#include <frozen/unordered_map.h> + +#include "mdnum.hxx" + +class MDTable; + +constexpr tools::Long MD_PARSPACE = o3tl::toTwips(5, o3tl::Length::mm); +constexpr tools::Long MD_MAX_IMAGE_WIDTH_IN_TWIPS = 2500; +constexpr tools::Long MD_MAX_IMAGE_HEIGH_IN_TWIPS = 2500; + +constexpr frozen::unordered_map<MD_ALIGN, SvxAdjust, 4> adjustMap + = { { MD_ALIGN_DEFAULT, SvxAdjust::Left }, + { MD_ALIGN_LEFT, SvxAdjust::Left }, + { MD_ALIGN_CENTER, SvxAdjust::Center }, + { MD_ALIGN_RIGHT, SvxAdjust::Right } }; + +constexpr Color COL_CODE_BLOCK = { 225, 225, 225 }; + +enum SwMdAppendMode +{ + AM_NORMAL, // no paragraph spacing handling + AM_NOSPACE, // set spacing hard to 0cm + AM_SPACE, // set spacing hard to 0.5cm + AM_SOFTNOSPACE, // don't set spacing, but save 0cm + AM_NONE // no append +}; + +typedef std::vector<std::unique_ptr<SfxPoolItem>> MDAttrStack; class SwMarkdownParser { @@ -34,18 +63,90 @@ class SwMarkdownParser SvStream& m_rInput; // SfxMedium* m_pMedium; std::unique_ptr<char[]> m_pArr; + std::unique_ptr<SwMdNumRuleInfo> m_pNumRuleInfo; tools::Long m_nFilesize; + MDAttrStack m_aAttrStack; + + OUString m_htmlData; + OUString m_sBaseURL; + + int m_nBlockQuoteDepth; + bool m_bNewDoc; + bool m_bNoParSpace : 1; + bool m_bInsideImage; + + std::vector<MDTable*> m_aTables; + std::shared_ptr<MDTable> m_xTable; SwMarkdownParser(const SwMarkdownParser&) = delete; SwMarkdownParser& operator=(const SwMarkdownParser&) = delete; + void SetNodeNum(sal_uInt8 nLevel); + + sal_Int32 StripTrailingLF(); + + bool AppendTextNode(SwMdAppendMode eMode, bool bUpdateNum = true); + void AddParSpace(); + + void AddBlockQuote(); + void EndBlockQuote(); + + void AddHR(); + void EndHR(); + + void StartPara(); + void EndPara(); + void StartHeading(sal_uInt8 nLvl); + void EndHeading(); + + void StartNumberedBulletList(MD_BLOCKTYPE aListType); + void EndNumberedBulletList(); + void StartNumberedBulletListItem(); + void EndNumberedBulletListItem(); + + void BeginHtmlBlock(); + void InsertHtmlData(); + void EndHtmlBlock(); + + void BeginCodeBlock(); + void EndCodeBlock(); + + void InsertText(OUString& aStr); + void SetAttrs(SwPaM& rRange); + void ClearAttrs(); + + void InsertImage(const OUString& aURL, const OUString& rTitle); + + void StartTable(sal_Int32 nRow, sal_Int32 nCol); + void EndTable(); + + void StartRow(); + + void StartCell(MD_ALIGN eAdjust); + +public: + void RegisterTable(MDTable* pTable); + void DeRegisterTable(MDTable* pTable); + public: - SwMarkdownParser(SwDoc& rD, SwPaM& rCursor, SvStream& rIn, bool bReadNewDoc); + SwMarkdownParser(SwDoc& rD, SwPaM& rCursor, SvStream& rIn, OUString aBaseURL, bool bReadNewDoc); + + SwMdNumRuleInfo& GetNumInfo() const { return *m_pNumRuleInfo; } bool IsNewDoc() const { return m_bNewDoc; } + ErrCode CallParser(); ~SwMarkdownParser(); + +private: + static int enter_block_callback(MD_BLOCKTYPE type, void* detail, void* userdata); + static int leave_block_callback(MD_BLOCKTYPE type, void* detail, void* userdata); + static int enter_span_callback(MD_SPANTYPE type, void* detail, void* userdata); + static int leave_span_callback(MD_SPANTYPE type, void* detail, void* userdata); + static int text_callback(MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size, void* userdata); + + static void SanitizeAnchor(SfxItemSet& rFrameItemSet); }; /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */