Compare commits

...

4 Commits

Author SHA1 Message Date
puz 5df4f3d840 Merge remote-tracking branch 'origin/dev_na' into dev_na 2026-07-14 14:32:02 +08:00
puz c8df0d9604 feat: PDF文档调整 2026-07-14 14:15:23 +08:00
puz cd4b5365d6 feat: word文档调整暂存2 2026-07-14 11:24:15 +08:00
puz 58674642e7 feat: word文档调整暂存 2026-07-13 18:07:57 +08:00
2 changed files with 506 additions and 198 deletions

View File

@ -16,9 +16,8 @@ import com.unisbase.service.TenantManagementService;
import lombok.RequiredArgsConstructor;
import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
@ -31,6 +30,8 @@ import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.parser.Tag;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
@ -38,6 +39,7 @@ import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@ -48,8 +50,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
@RequiredArgsConstructor
@ -62,6 +62,8 @@ public class MeetingExportServiceImpl implements MeetingExportService {
private static final float WATERMARK_LINE_GAP = 34f;
private static final float WATERMARK_BASE_STEP_X = 430f;
private static final float WATERMARK_BASE_STEP_Y = 300f;
private static final String PDF_EXPORT_VERSION_PROPERTY = "iMeetingPdfExportVersion";
private static final String PDF_EXPORT_VERSION = "3";
private final MeetingSummaryFileService meetingSummaryFileService;
private final SysTenantUserService sysTenantUserService;
@ -96,110 +98,64 @@ public class MeetingExportServiceImpl implements MeetingExportService {
Files.createDirectories(exportDir);
Path exportPath = exportDir.resolve("summary." + ext);
boolean isPdf = "pdf".equals(ext);
boolean isPdf = "pdf".equals(ext);
boolean needRegenerate = !Files.exists(exportPath)
|| Files.getLastModifiedTime(exportPath).toMillis() < Files.getLastModifiedTime(summarySourcePath).toMillis();
if (!needRegenerate && "docx".equals(ext) && !isCurrentWordExport(exportPath)) {
needRegenerate = true;
}
if (!needRegenerate && isPdf && !isCurrentPdfExport(exportPath)) {
needRegenerate = true;
}
byte[] baseBytes;
byte[] baseBytes;
if (needRegenerate) {
String markdown = Files.readString(summarySourcePath, StandardCharsets.UTF_8);
meetingDetail.setSummaryContent(meetingSummaryFileService.stripFrontMatter(markdown));
baseBytes = "docx".equals(ext) ? buildWordBytes(meetingDetail) : buildPdfBytes(meetingDetail);
Files.write(exportPath, baseBytes);
baseBytes = "docx".equals(ext)
? new MeetingWordDocumentBuilder().build(meetingDetail)
: buildPdfBytes(meetingDetail);
Files.write(exportPath, baseBytes);
} else {
baseBytes = Files.readAllBytes(exportPath);
baseBytes = Files.readAllBytes(exportPath);
}
byte[] bytes = isPdf
? applyPdfWatermark(baseBytes, resolveWatermarkText(loginUser))
: baseBytes;
return new MeetingSummaryExportResult(bytes, contentType, safeTitle + "-AI-Summary." + ext);
return new MeetingSummaryExportResult(bytes, contentType, safeTitle + "." + ext);
} catch (IOException ex) {
throw new RuntimeException("导出失败:" + ex.getMessage(), ex);
}
}
private byte[] buildWordBytes(MeetingVO meeting) throws IOException {
try (XWPFDocument document = new XWPFDocument();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
XWPFParagraph title = document.createParagraph();
XWPFRun titleRun = title.createRun();
titleRun.setBold(true);
titleRun.setFontSize(16);
titleRun.setText((meeting.getTitle() == null ? "Meeting" : meeting.getTitle()) + " - AI Summary");
private boolean isCurrentWordExport(Path exportPath) {
try (InputStream input = Files.newInputStream(exportPath);
XWPFDocument document = new XWPFDocument(input)) {
var property = document.getProperties()
.getCustomProperties()
.getProperty(MeetingWordDocumentBuilder.EXPORT_VERSION_PROPERTY);
return property != null
&& property.isSetLpwstr()
&& MeetingWordDocumentBuilder.EXPORT_VERSION.equals(property.getLpwstr());
} catch (Exception ignored) {
return false;
}
}
XWPFParagraph timeP = document.createParagraph();
timeP.createRun().setText("Meeting Time: " + String.valueOf(meeting.getMeetingTime()));
XWPFParagraph hostP = document.createParagraph();
hostP.createRun().setText("Host: " + (meeting.getHostName() == null ? "" : meeting.getHostName()));
XWPFParagraph participantsP = document.createParagraph();
participantsP.createRun().setText("Participants: " + (meeting.getParticipants() == null ? "" : meeting.getParticipants()));
document.createParagraph();
for (MdBlock block : parseMarkdownBlocks(meeting.getSummaryContent())) {
XWPFParagraph p = document.createParagraph();
if (block.type == MdType.HEADING) {
int size = Math.max(12, 18 - (block.level - 1) * 2);
appendMarkdownRuns(p, block.text, true, size);
} else if (block.type == MdType.LIST) {
p.setIndentationLeft(360);
XWPFRun bullet = p.createRun();
bullet.setFontSize(12);
bullet.setText("- ");
appendMarkdownRuns(p, block.text, false, 12);
} else {
appendMarkdownRuns(p, block.text, false, 12);
}
}
document.write(out);
return out.toByteArray();
private boolean isCurrentPdfExport(Path exportPath) {
try (PDDocument document = PDDocument.load(exportPath.toFile())) {
String version = document.getDocumentInformation()
.getCustomMetadataValue(PDF_EXPORT_VERSION_PROPERTY);
return PDF_EXPORT_VERSION.equals(version);
} catch (Exception ignored) {
return false;
}
}
private byte[] buildPdfBytes(MeetingVO meeting) throws IOException {
Parser parser = Parser.builder().build();
String markdown = meeting.getSummaryContent() == null ? "" : meeting.getSummaryContent();
Node document = parser.parse(markdown);
HtmlRenderer renderer = HtmlRenderer.builder().build();
String htmlBody = renderer.render(document);
String title = meeting.getTitle() == null ? "Meeting" : meeting.getTitle();
String time = meeting.getMeetingTime() == null ? "" : meeting.getMeetingTime().toString();
String host = meeting.getHostName() == null ? "" : meeting.getHostName();
String participants = meeting.getParticipants() == null ? "" : meeting.getParticipants();
String html = "<html><head><style>" +
"body { font-family: 'NotoSansSC', 'SimSun', sans-serif; padding: 20px; line-height: 1.6; color: #333; }" +
"h1, h2, h3 { color: #1890ff; border-bottom: 1px solid #eee; padding-bottom: 5px; }" +
"table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }" +
"table, th, td { border: 1px solid #ddd; }" +
"th, td { padding: 8px 12px; text-align: left; }" +
"th { background-color: #f5f5f5; font-weight: bold; }" +
"blockquote { padding: 8px 16px; color: #666; border-left: 4px solid #1890ff; background: #f0f7ff; margin: 0 0 16px 0; }" +
"</style></head><body>" +
"<div style='text-align:center; margin-bottom:30px; border-bottom: 2px solid #1890ff; padding-bottom:20px;'>" +
"<h1 style='font-size:28px; margin-bottom:12px; color:#000; border:none;'>" + title + "</h1>" +
"<div style='font-size:14px; color:#666;'>" +
"<span>Meeting Time: " + time + "</span>" +
"<span style='margin: 0 20px;'>|</span>" +
"<span>Host: " + host + "</span>" +
"<span style='margin: 0 20px;'>|</span>" +
"<span>Participants: " + participants + "</span>" +
"</div></div>" +
"<div class='markdown-body'>" + htmlBody + "</div>" +
"<div style='margin-top: 40px; text-align: right; font-size: 12px; color: #999; border-top: 1px dashed #eee; padding-top: 10px;'>" +
"Generated by iMeeting AI Assistant" +
"</div>" +
"</body></html>";
org.jsoup.nodes.Document jsoupDoc = Jsoup.parse(html);
jsoupDoc.outputSettings().syntax(org.jsoup.nodes.Document.OutputSettings.Syntax.xml);
String xhtml = jsoupDoc.html();
String xhtml = buildPdfXhtml(meeting);
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
PdfRendererBuilder builder = new PdfRendererBuilder();
@ -229,12 +185,134 @@ public class MeetingExportServiceImpl implements MeetingExportService {
builder.withHtmlContent(xhtml, null);
builder.toStream(out);
builder.run();
return out.toByteArray();
return markCurrentPdfExport(out.toByteArray());
} catch (Exception ex) {
throw new IOException("PDF 生成失败", ex);
}
}
private byte[] markCurrentPdfExport(byte[] pdfBytes) throws IOException {
try (PDDocument document = PDDocument.load(pdfBytes);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
PDDocumentInformation information = document.getDocumentInformation();
information.setCustomMetadataValue(PDF_EXPORT_VERSION_PROPERTY, PDF_EXPORT_VERSION);
document.setDocumentInformation(information);
document.save(out);
return out.toByteArray();
}
}
String buildPdfXhtml(MeetingVO meeting) {
Parser parser = Parser.builder().build();
String markdown = meeting.getSummaryContent() == null ? "" : meeting.getSummaryContent();
Node document = parser.parse(markdown);
HtmlRenderer renderer = HtmlRenderer.builder().build();
String htmlBody = renderer.render(document);
String title = meeting.getTitle() == null ? "Meeting" : meeting.getTitle();
String time = meeting.getMeetingTime() == null ? "" : meeting.getMeetingTime().toString();
String host = meeting.getHostName() == null ? "" : meeting.getHostName();
String participants = meeting.getParticipants() == null ? "" : meeting.getParticipants();
String html = "<html><head><style>" +
"body { font-family: 'NotoSansSC', 'SimSun', sans-serif; padding: 20px; line-height: 1.6; color: #333; }" +
"h1, h2, h3 { color: #1890ff; border-bottom: 1px solid #eee; padding-bottom: 5px; }" +
".markdown-body { font-size: 14px; }" +
".markdown-body p { margin: 0 0 8px 0; }" +
".pdf-list-item { margin: 2px 0 4px 0; padding-left: 10px; text-indent: -10px; line-height: 1.6; }" +
".pdf-list-level-1 { margin-left: 22px; }" +
".pdf-list-level-2 { margin-left: 44px; }" +
".pdf-list-level-3 { margin-left: 66px; }" +
".pdf-list-marker { display: inline-block; width: 10px; font-size: 12px; line-height: 1; vertical-align: 0.06em; color: #333; }" +
".pdf-list-content { display: inline; }" +
"table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }" +
"table, th, td { border: 1px solid #ddd; }" +
"th, td { padding: 8px 12px; text-align: left; }" +
"th { background-color: #f5f5f5; font-weight: bold; }" +
"blockquote { padding: 8px 16px; color: #666; border-left: 4px solid #1890ff; background: #f0f7ff; margin: 0 0 16px 0; }" +
"</style></head><body>" +
"<div style='text-align:center; margin-bottom:30px; border-bottom: 2px solid #1890ff; padding-bottom:20px;'>" +
"<h1 style='font-size:28px; margin-bottom:12px; color:#000; border:none;'>" + title + "</h1>" +
"<div style='font-size:14px; color:#666;'>" +
"<span>Meeting Time: " + time + "</span>" +
"<span style='margin: 0 20px;'>|</span>" +
"<span>Host: " + host + "</span>" +
"<span style='margin: 0 20px;'>|</span>" +
"<span>Participants: " + participants + "</span>" +
"</div></div>" +
"<div class='markdown-body'>" + htmlBody + "</div>" +
"<div style='margin-top: 40px; text-align: right; font-size: 12px; color: #999; border-top: 1px dashed #eee; padding-top: 10px;'>" +
"Generated by iMeeting AI Assistant" +
"</div>" +
"</body></html>";
org.jsoup.nodes.Document jsoupDoc = Jsoup.parse(html);
normalizeMarkdownLists(jsoupDoc.selectFirst(".markdown-body"), 0);
jsoupDoc.outputSettings().syntax(org.jsoup.nodes.Document.OutputSettings.Syntax.xml);
return jsoupDoc.html();
}
private void normalizeMarkdownLists(Element container, int level) {
if (container == null) {
return;
}
for (Element child : new ArrayList<>(container.children())) {
if (isListElement(child)) {
for (org.jsoup.nodes.Node node : toPdfListRows(child, level)) {
child.before(node);
}
child.remove();
} else {
normalizeMarkdownLists(child, level);
}
}
}
private List<org.jsoup.nodes.Node> toPdfListRows(Element list, int level) {
List<org.jsoup.nodes.Node> rows = new ArrayList<>();
for (Element item : list.children()) {
if (!"li".equalsIgnoreCase(item.tagName())) {
continue;
}
Element row = new Element(Tag.valueOf("p"), "");
row.addClass("pdf-list-item");
row.addClass("pdf-list-level-" + Math.min(level, 3));
Element marker = new Element(Tag.valueOf("span"), "");
marker.addClass("pdf-list-marker");
marker.text("•");
row.appendChild(marker);
Element content = new Element(Tag.valueOf("span"), "");
content.addClass("pdf-list-content");
List<Element> nestedLists = new ArrayList<>();
for (org.jsoup.nodes.Node node : new ArrayList<>(item.childNodes())) {
if (node instanceof Element element && isListElement(element)) {
nestedLists.add(element);
} else if (node instanceof Element element && "p".equalsIgnoreCase(element.tagName())) {
for (org.jsoup.nodes.Node paragraphNode : new ArrayList<>(element.childNodes())) {
content.appendChild(paragraphNode);
}
element.remove();
} else {
content.appendChild(node);
}
}
row.appendChild(content);
rows.add(row);
for (Element nestedList : nestedLists) {
rows.addAll(toPdfListRows(nestedList, level + 1));
}
}
return rows;
}
private boolean isListElement(Element element) {
return "ul".equalsIgnoreCase(element.tagName()) || "ol".equalsIgnoreCase(element.tagName());
}
private byte[] applyPdfWatermark(byte[] pdfBytes, String watermarkText) throws IOException {
if (watermarkText == null || watermarkText.isBlank()) {
return pdfBytes;
@ -419,116 +497,4 @@ public class MeetingExportServiceImpl implements MeetingExportService {
}
}
private void appendMarkdownRuns(XWPFParagraph paragraph, String text, boolean defaultBold, int size) {
String input = text == null ? "" : text;
Matcher matcher = Pattern.compile("\\*\\*(.+?)\\*\\*").matcher(input);
int start = 0;
while (matcher.find()) {
String normal = toPlainInline(input.substring(start, matcher.start()));
if (!normal.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setBold(defaultBold);
run.setFontSize(size);
run.setText(normal);
}
String boldText = toPlainInline(matcher.group(1));
if (!boldText.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setBold(true);
run.setFontSize(size);
run.setText(boldText);
}
start = matcher.end();
}
String tail = toPlainInline(input.substring(start));
if (!tail.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setBold(defaultBold);
run.setFontSize(size);
run.setText(tail);
}
}
private List<MdBlock> parseMarkdownBlocks(String markdown) {
List<MdBlock> blocks = new ArrayList<>();
if (markdown == null || markdown.trim().isEmpty()) {
return blocks;
}
String[] lines = markdown.replace("\r\n", "\n").split("\n");
StringBuilder paragraph = new StringBuilder();
for (String raw : lines) {
String line = raw == null ? "" : raw.trim();
if (line.isEmpty()) {
flushParagraph(blocks, paragraph);
continue;
}
if (line.startsWith("#")) {
flushParagraph(blocks, paragraph);
int level = 0;
while (level < line.length() && line.charAt(level) == '#') {
level++;
}
level = Math.min(level, 6);
blocks.add(new MdBlock(MdType.HEADING, level, line.substring(level).trim()));
continue;
}
if (line.startsWith("- ") || line.startsWith("* ")) {
flushParagraph(blocks, paragraph);
blocks.add(new MdBlock(MdType.LIST, 0, line.substring(2).trim()));
continue;
}
Matcher ordered = Pattern.compile("^\\d+\\.\\s+(.*)$").matcher(line);
if (ordered.find()) {
flushParagraph(blocks, paragraph);
blocks.add(new MdBlock(MdType.LIST, 0, ordered.group(1).trim()));
continue;
}
if (paragraph.length() > 0) {
paragraph.append(' ');
}
paragraph.append(line);
}
flushParagraph(blocks, paragraph);
return blocks;
}
private void flushParagraph(List<MdBlock> blocks, StringBuilder paragraph) {
if (paragraph.length() > 0) {
blocks.add(new MdBlock(MdType.PARAGRAPH, 0, paragraph.toString()));
paragraph.setLength(0);
}
}
private String toPlainInline(String input) {
if (input == null) {
return "";
}
return input
.replaceAll("`([^`]+)`", "$1")
.replaceAll("\\*\\*(.*?)\\*\\*", "$1")
.replaceAll("\\*(.*?)\\*", "$1")
.replaceAll("\\[(.*?)]\\((.*?)\\)", "$1");
}
private enum MdType {
HEADING,
LIST,
PARAGRAPH
}
private static class MdBlock {
private final MdType type;
private final int level;
private final String text;
private MdBlock(MdType type, int level, String text) {
this.type = type;
this.level = level;
this.text = text;
}
}
}

View File

@ -0,0 +1,342 @@
package com.imeeting.service.biz.impl;
import com.imeeting.dto.biz.MeetingVO;
import org.apache.poi.xwpf.usermodel.LineSpacingRule;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STPageOrientation;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class MeetingWordDocumentBuilder {
static final String EXPORT_VERSION_PROPERTY = "iMeetingWordExportVersion";
static final String EXPORT_VERSION = "12";
private static final String DOCUMENT_FONT = "仿宋_GB2312";
private static final int BODY_FONT_SIZE = 14;
private static final int BODY_FIRST_LINE_INDENT = 640;
private static final int BODY_LINE_SPACING = 29;
private static final DateTimeFormatter MEETING_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private static final Pattern BODY_STYLE_LIST_PREFIX = Pattern.compile(
"^\\*\\*([^*\\r\\n:]+?)(?:\\*\\*[\\h\\u200B]*([:])|([:])[\\h\\u200B]*\\*\\*)"
);
private static final Pattern FULLY_BOLD_LIST_ITEM = Pattern.compile(
"^\\*\\*([^*\\r\\n]+)\\*\\*$"
);
private static final Pattern QUOTE_PREFIX = Pattern.compile("^[>]+[\\h\\u200B]*");
private static final Pattern HORIZONTAL_RULE = Pattern.compile("^[-*_](?:[\\h\\u200B]*[-*_]){2,}$");
private static final Pattern MIXED_CONTENT_SPACING = Pattern.compile(
"(?<=\\p{IsHan})[\\h\\u200B]+(?=[\\p{IsLatin}\\p{N}])"
+ "|(?<=[\\p{IsLatin}\\p{N}])[\\h\\u200B]+(?=\\p{IsHan})"
);
byte[] build(MeetingVO meeting) throws IOException {
try (XWPFDocument document = new XWPFDocument();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
document.getProperties().getCustomProperties()
.addProperty(EXPORT_VERSION_PROPERTY, EXPORT_VERSION);
applyPageLayout(document);
XWPFParagraph title = document.createParagraph();
title.setAlignment(ParagraphAlignment.CENTER);
title.setSpacingAfter(240);
XWPFRun titleRun = title.createRun();
titleRun.setBold(true);
titleRun.setFontSize(22);
titleRun.setText(meeting.getTitle() == null ? "Meeting" : meeting.getTitle());
XWPFParagraph timeP = document.createParagraph();
appendMetadata(timeP, "会议时间:" + formatMeetingTime(meeting));
XWPFParagraph separator = document.createParagraph();
separator.setSpacingAfter(120);
String summaryContent = normalizeMixedContentSpacing(meeting.getSummaryContent());
for (MdBlock block : parseMarkdownBlocks(summaryContent)) {
XWPFParagraph paragraph = document.createParagraph();
if (block.type == MdType.HEADING) {
applyHeadingParagraphStyle(paragraph);
int size = Math.max(12, 16 - (block.level - 1) * 2);
appendMarkdownRuns(paragraph, block.text, true, size);
} else if (block.type == MdType.QUOTE) {
applyBodyParagraphStyle(paragraph);
appendPlainText(paragraph, block.text, BODY_FONT_SIZE);
} else if (block.type == MdType.LIST && isBodyStyleListItem(block.text)) {
applyBodyParagraphStyle(paragraph);
appendMarkdownRuns(paragraph, demoteBodyStyleListPrefix(block.text), false, BODY_FONT_SIZE);
} else if (block.type == MdType.LIST) {
applyBodyParagraphStyle(paragraph);
appendMarkdownRuns(paragraph, block.text, false, BODY_FONT_SIZE);
} else {
applyBodyParagraphStyle(paragraph);
appendMarkdownRuns(paragraph, block.text, false, BODY_FONT_SIZE);
}
}
appendGeneratedByFooter(document);
applyDocumentFont(document);
document.write(out);
return out.toByteArray();
}
}
private String formatMeetingTime(MeetingVO meeting) {
return meeting.getMeetingTime() == null ? "" : meeting.getMeetingTime().format(MEETING_TIME_FORMATTER);
}
private String normalizeMixedContentSpacing(String markdown) {
if (markdown == null) {
return null;
}
return MIXED_CONTENT_SPACING.matcher(markdown).replaceAll("");
}
private boolean isBodyStyleListItem(String text) {
if (text == null) {
return false;
}
Matcher prefixMatcher = BODY_STYLE_LIST_PREFIX.matcher(text);
if (prefixMatcher.find() && !text.substring(prefixMatcher.end()).isBlank()) {
return true;
}
Matcher fullBoldMatcher = FULLY_BOLD_LIST_ITEM.matcher(text);
if (!fullBoldMatcher.matches()) {
return false;
}
String plainText = fullBoldMatcher.group(1).trim();
int colonIndex = firstTitleColonIndex(plainText);
return colonIndex > 0 && colonIndex < plainText.length() - 1;
}
private String demoteBodyStyleListPrefix(String text) {
return toPlainInline(text).trim()
.replaceFirst("[\\h\\u200B]*([:])[\\h\\u200B]*", "$1");
}
private int firstTitleColonIndex(String text) {
for (int index = 0; index < text.length(); index++) {
char current = text.charAt(index);
if (current == '') {
return index;
}
if (current == ':'
&& (index == 0 || !Character.isDigit(text.charAt(index - 1)))
&& (index == text.length() - 1 || !Character.isDigit(text.charAt(index + 1)))) {
return index;
}
}
return -1;
}
private void applyBodyParagraphStyle(XWPFParagraph paragraph) {
paragraph.setAlignment(ParagraphAlignment.BOTH);
paragraph.setFirstLineIndent(BODY_FIRST_LINE_INDENT);
paragraph.setSpacingBetween(BODY_LINE_SPACING, LineSpacingRule.EXACT);
paragraph.setSpacingBefore(0);
paragraph.setSpacingAfter(0);
}
private void applyHeadingParagraphStyle(XWPFParagraph paragraph) {
if (!paragraph.getCTP().isSetPPr()) {
paragraph.getCTP().addNewPPr();
}
paragraph.setKeepNext(true);
paragraph.setSpacingBefore(240);
paragraph.setSpacingAfter(120);
}
private void appendMetadata(XWPFParagraph paragraph, String text) {
paragraph.setAlignment(ParagraphAlignment.RIGHT);
paragraph.setSpacingAfter(80);
XWPFRun run = paragraph.createRun();
run.setFontSize(12);
run.setText(text);
}
private void appendPlainText(XWPFParagraph paragraph, String text, int size) {
String plainText = toPlainInline(text);
if (plainText.isEmpty()) {
return;
}
XWPFRun run = paragraph.createRun();
run.setFontSize(size);
run.setText(plainText);
}
private void appendGeneratedByFooter(XWPFDocument document) {
XWPFParagraph paragraph = document.createParagraph();
paragraph.setAlignment(ParagraphAlignment.CENTER);
paragraph.setSpacingBefore(480);
paragraph.setSpacingAfter(0);
XWPFRun run = paragraph.createRun();
run.setItalic(true);
run.setFontSize(10);
run.setText("—— 内容由智听云AI生成 ——");
}
private void applyPageLayout(XWPFDocument document) {
CTBody body = document.getDocument().getBody();
CTSectPr section = body.isSetSectPr() ? body.getSectPr() : body.addNewSectPr();
CTPageSz pageSize = section.isSetPgSz() ? section.getPgSz() : section.addNewPgSz();
pageSize.setW(BigInteger.valueOf(11906));
pageSize.setH(BigInteger.valueOf(16838));
pageSize.setOrient(STPageOrientation.PORTRAIT);
CTPageMar margins = section.isSetPgMar() ? section.getPgMar() : section.addNewPgMar();
margins.setTop(BigInteger.valueOf(2098));
margins.setRight(BigInteger.valueOf(1531));
margins.setBottom(BigInteger.valueOf(1871));
margins.setLeft(BigInteger.valueOf(1531));
margins.setHeader(BigInteger.valueOf(708));
margins.setFooter(BigInteger.valueOf(708));
margins.setGutter(BigInteger.ZERO);
}
private void applyDocumentFont(XWPFDocument document) {
for (XWPFParagraph paragraph : document.getParagraphs()) {
for (XWPFRun run : paragraph.getRuns()) {
run.setFontFamily(DOCUMENT_FONT, XWPFRun.FontCharRange.eastAsia);
run.setFontFamily(DOCUMENT_FONT, XWPFRun.FontCharRange.ascii);
run.setFontFamily(DOCUMENT_FONT, XWPFRun.FontCharRange.hAnsi);
run.setFontFamily(DOCUMENT_FONT, XWPFRun.FontCharRange.cs);
}
}
}
private void appendMarkdownRuns(XWPFParagraph paragraph, String text, boolean defaultBold, int size) {
String input = text == null ? "" : text;
Matcher matcher = Pattern.compile("\\*\\*(.+?)\\*\\*").matcher(input);
int start = 0;
while (matcher.find()) {
String normal = toPlainInline(input.substring(start, matcher.start()));
if (!normal.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setBold(defaultBold);
run.setFontSize(size);
run.setText(normal);
}
String boldText = toPlainInline(matcher.group(1));
if (!boldText.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setBold(true);
run.setFontSize(size);
run.setText(boldText);
}
start = matcher.end();
}
String tail = toPlainInline(input.substring(start));
if (!tail.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setBold(defaultBold);
run.setFontSize(size);
run.setText(tail);
}
}
private List<MdBlock> parseMarkdownBlocks(String markdown) {
List<MdBlock> blocks = new ArrayList<>();
if (markdown == null || markdown.trim().isEmpty()) {
return blocks;
}
String[] lines = markdown.replace("\r\n", "\n").split("\n");
StringBuilder paragraph = new StringBuilder();
for (String raw : lines) {
String line = raw == null ? "" : raw.trim();
if (line.isEmpty()) {
flushParagraph(blocks, paragraph);
continue;
}
if (HORIZONTAL_RULE.matcher(line).matches()) {
flushParagraph(blocks, paragraph);
continue;
}
if (line.startsWith("#")) {
flushParagraph(blocks, paragraph);
int level = 0;
while (level < line.length() && line.charAt(level) == '#') {
level++;
}
level = Math.min(level, 6);
blocks.add(new MdBlock(MdType.HEADING, level, line.substring(level).trim()));
continue;
}
if (line.startsWith(">") || line.startsWith("")) {
flushParagraph(blocks, paragraph);
blocks.add(new MdBlock(MdType.QUOTE, 0, QUOTE_PREFIX.matcher(line).replaceFirst("")));
continue;
}
if (line.startsWith("- ") || line.startsWith("* ")) {
flushParagraph(blocks, paragraph);
blocks.add(new MdBlock(MdType.LIST, 0, line.substring(2).trim()));
continue;
}
Matcher ordered = Pattern.compile("^\\d+\\.\\s+(.*)$").matcher(line);
if (ordered.find()) {
flushParagraph(blocks, paragraph);
blocks.add(new MdBlock(MdType.LIST, 0, ordered.group(1).trim()));
continue;
}
if (paragraph.length() > 0) {
paragraph.append(' ');
}
paragraph.append(line);
}
flushParagraph(blocks, paragraph);
return blocks;
}
private void flushParagraph(List<MdBlock> blocks, StringBuilder paragraph) {
if (paragraph.length() > 0) {
blocks.add(new MdBlock(MdType.PARAGRAPH, 0, paragraph.toString()));
paragraph.setLength(0);
}
}
private String toPlainInline(String input) {
if (input == null) {
return "";
}
return input
.replaceAll("`([^`]+)`", "$1")
.replaceAll("\\*\\*(.*?)\\*\\*", "$1")
.replaceAll("\\*(.*?)\\*", "$1")
.replaceAll("\\[(.*?)]\\((.*?)\\)", "$1");
}
private enum MdType {
HEADING,
QUOTE,
LIST,
PARAGRAPH
}
private static class MdBlock {
private final MdType type;
private final int level;
private final String text;
private MdBlock(MdType type, int level, String text) {
this.type = type;
this.level = level;
this.text = text;
}
}
}