diff --git a/fs-admin/src/main/java/com/xddcodec/fs/aspectj/RequestLoggingAspect.java b/fs-admin/src/main/java/com/xddcodec/fs/aspectj/RequestLoggingAspect.java
index 1d0efbaf3291b4ab2d6e82a4cd14f7975c360e22..593ef54f4d2a811b7ce6a45dd28e5ed9aaba0c00 100644
--- a/fs-admin/src/main/java/com/xddcodec/fs/aspectj/RequestLoggingAspect.java
+++ b/fs-admin/src/main/java/com/xddcodec/fs/aspectj/RequestLoggingAspect.java
@@ -29,7 +29,8 @@ public class RequestLoggingAspect {
* 定义切点
*/
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
- public void controllerLog() {}
+ public void controllerLog() {
+ }
/**
* 方法执行前日志
diff --git a/fs-admin/src/main/resources/application-dev.yml b/fs-admin/src/main/resources/application-dev.yml
index 1e9ecb624846551332ed7f6391388db93cbda9f5..c4374d0d315ce635c036ec208ec0d62452df4754 100644
--- a/fs-admin/src/main/resources/application-dev.yml
+++ b/fs-admin/src/main/resources/application-dev.yml
@@ -27,13 +27,13 @@ spring:
redis:
host: 127.0.0.1
port: 6379
- password: 123456
+ password: insentek
database: 0
# 连接超时时间
timeout: 10s
lettuce:
pool:
- # 连接池最大连接数
+ # 连接池最大连接数f
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
@@ -54,12 +54,28 @@ redisson:
config: |
singleServerConfig:
address: "redis://127.0.0.1:6379"
- password: "123456"
+ password: "insentek"
database: 0
timeout: 3000
connectionPoolSize: 64
connectionMinimumIdleSize: 10
+fs:
+ preview:
+ # 预览文件最大大小(字节),默认500MB
+ max-file-size: 524288000
+
+ # 单次Range请求最大大小(字节),默认10MB
+ # 用于视频/音频的分片传输,防止内存溢出
+ max-range-size: 10485760
+
+ # 小文件直接传输阈值(字节),默认10MB
+ # 小于此大小的文件将一次性读取到内存
+ small-file-size: 10485760
+
+ # 缓冲区大小(字节),默认8KB
+ buffer-size: 8192
+
mybatis-flex:
# sql审计
audit_enable: true
diff --git a/fs-admin/src/main/resources/application.yml b/fs-admin/src/main/resources/application.yml
index a82b67d42992366fc8df153a434e01f39ac23f20..735af64a6383a903a1639c64c9a4e8b2aae168c6 100644
--- a/fs-admin/src/main/resources/application.yml
+++ b/fs-admin/src/main/resources/application.yml
@@ -117,7 +117,6 @@ security:
- /apis/user/register
- /apis/user/forget-password/code/**
- /apis/user/forget-password
-# - /apis/share/**
--- #Sa-Token 配置
sa-token:
diff --git a/fs-admin/src/main/resources/static/favicon.ico b/fs-admin/src/main/resources/static/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..9f1881facce5b9a0701b5a4a54c73c3013c8a3d9
Binary files /dev/null and b/fs-admin/src/main/resources/static/favicon.ico differ
diff --git a/fs-dependencies/pom.xml b/fs-dependencies/pom.xml
index 2b3032c38d9f9543915dd02927c3c113ca4d4aba..ba34a0a68fdf138fc5e880038e3c8f78897c2144 100644
--- a/fs-dependencies/pom.xml
+++ b/fs-dependencies/pom.xml
@@ -124,6 +124,11 @@
fs-swagger
${revision}
+
+ com.xddcodec.fs
+ fs-preview
+ ${revision}
+
com.xddcodec.fs
storage-plugin-boot
diff --git a/fs-framework/fs-common-core/src/main/java/com/xddcodec/fs/framework/common/enums/FileTypeEnum.java b/fs-framework/fs-common-core/src/main/java/com/xddcodec/fs/framework/common/enums/FileTypeEnum.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac65f240300a9304d07205e47738d65c48c46bd1
--- /dev/null
+++ b/fs-framework/fs-common-core/src/main/java/com/xddcodec/fs/framework/common/enums/FileTypeEnum.java
@@ -0,0 +1,329 @@
+package com.xddcodec.fs.framework.common.enums;
+
+import lombok.Getter;
+
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 文件类型枚举(融合预览功能)
+ *
+ * @Author: xddcode
+ * @Date: 2024/12/26
+ */
+@Getter
+public enum FileTypeEnum {
+
+ // ==================== 图片类型 ====================
+ IMAGE("image", "图片", FileCategory.IMAGE,
+ Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp", "svg",
+ "JPG", "JPEG", "PNG", "GIF", "BMP", "WEBP", "SVG"),
+ true, "preview/image", false),
+
+ // ==================== 视频类型 ====================
+ VIDEO("video", "视频", FileCategory.VIDEO,
+ Arrays.asList("mp4", "avi", "mkv", "mov", "wmv", "flv", "webm",
+ "MP4", "AVI", "MKV", "MOV", "WMV", "FLV", "WEBM"),
+ true, "preview/video", false),
+
+ // ==================== 音频类型 ====================
+ AUDIO("audio", "音频", FileCategory.AUDIO,
+ Arrays.asList("mp3", "wav", "flac", "aac", "ogg", "m4a", "wma",
+ "MP3", "WAV", "FLAC", "AAC", "OGG", "M4A", "WMA"),
+ true, "preview/audio", false),
+
+ // ==================== 文档类型 ====================
+ PDF("pdf", "PDF文档", FileCategory.DOCUMENT,
+ Arrays.asList("pdf", "PDF"),
+ true, "preview/pdf", false),
+
+ WORD("word", "Word文档", FileCategory.DOCUMENT,
+ Arrays.asList("doc", "docx", "DOC", "DOCX"),
+ true, "preview/pdf", true),
+
+ EXCEL("excel", "Excel表格", FileCategory.DOCUMENT,
+ Arrays.asList("xls", "xlsx", "XLS", "XLSX"),
+ true, "preview/pdf", true),
+
+ PPT("ppt", "PPT演示", FileCategory.DOCUMENT,
+ Arrays.asList("ppt", "pptx", "PPT", "PPTX"),
+ true, "preview/pdf", true),
+
+ // ==================== 文本类型 ====================
+ TEXT("text", "文本文件", FileCategory.DOCUMENT,
+ Arrays.asList("txt", "log", "ini", "properties", "yaml", "yml", "conf",
+ "TXT", "LOG", "INI", "PROPERTIES", "YAML", "YML", "CONF"),
+ true, "preview/text", false),
+
+ // ==================== 代码类型 ====================
+ CODE("code", "代码文件", FileCategory.DOCUMENT,
+ Arrays.asList(
+ // Java系
+ "java", "JAVA",
+ // JavaScript/TypeScript
+ "js", "jsx", "ts", "tsx", "JS", "JSX", "TS", "TSX",
+ // Python
+ "py", "PY",
+ // C/C++
+ "c", "cpp", "h", "hpp", "cc", "cxx",
+ "C", "CPP", "H", "HPP", "CC", "CXX",
+ // Web
+ "html", "css", "scss", "sass", "less", "vue",
+ "HTML", "CSS", "SCSS", "SASS", "LESS", "VUE",
+ // 其他语言
+ "php", "go", "rs", "rb", "swift", "kt", "scala",
+ "PHP", "GO", "RS", "RB", "SWIFT", "KT", "SCALA",
+ // 配置/脚本
+ "json", "xml", "sql", "sh", "bash", "bat", "ps1",
+ "JSON", "XML", "SQL", "SH", "BASH", "BAT", "PS1",
+ // C#/.NET
+ "cs", "CS",
+ // Rust
+ "toml", "TOML"
+ ),
+ true, "preview/code", false),
+
+ // ==================== Markdown ====================
+ MARKDOWN("markdown", "Markdown", FileCategory.DOCUMENT,
+ Arrays.asList("md", "markdown", "MD", "MARKDOWN"),
+ true, "preview/markdown", false),
+
+ // ==================== 压缩包 ====================
+ ARCHIVE("archive", "压缩包", FileCategory.OTHER,
+ Arrays.asList("zip", "rar", "7z", "tar", "gz", "bz2",
+ "ZIP", "RAR", "7Z", "TAR", "GZ", "BZ2"),
+ true, "preview/archive", false),
+
+ // ==================== 其他 ====================
+ OTHER("other", "其他", FileCategory.OTHER,
+ null, false, "preview/unsupported", false);
+
+ /**
+ * 类型标识(唯一)
+ */
+ private final String code;
+
+ /**
+ * 类型名称
+ */
+ private final String name;
+
+ /**
+ * 所属大类
+ */
+ private final FileCategory category;
+
+ /**
+ * 支持的文件后缀列表
+ */
+ private final List suffixes;
+
+ /**
+ * 是否支持预览
+ */
+ private final Boolean previewable;
+
+ /**
+ * 预览模板路径
+ */
+ private final String previewTemplate;
+
+ /**
+ * 是否需要转换(如Office转PDF)
+ */
+ private final Boolean needConvert;
+
+ FileTypeEnum(String code, String name, FileCategory category,
+ List suffixes, Boolean previewable,
+ String previewTemplate, Boolean needConvert) {
+ this.code = code;
+ this.name = name;
+ this.category = category;
+ this.suffixes = suffixes;
+ this.previewable = previewable;
+ this.previewTemplate = previewTemplate;
+ this.needConvert = needConvert;
+ }
+
+ private static final Map EXTENSION_MAP = new HashMap<>();
+ private static final Map> CATEGORY_MAP = new EnumMap<>(FileCategory.class);
+
+ static {
+ // 构建扩展名映射
+ for (FileTypeEnum type : values()) {
+ if (type.getSuffixes() != null) {
+ for (String ext : type.getSuffixes()) {
+ EXTENSION_MAP.put(ext.toLowerCase(), type);
+ }
+ }
+ }
+
+ // 构建分类映射
+ for (FileTypeEnum type : values()) {
+ CATEGORY_MAP.computeIfAbsent(type.category, k -> new ArrayList<>()).add(type);
+ }
+ }
+
+ /**
+ * 根据类型标识获取枚举(支持英文code或中文name)
+ */
+ public static FileTypeEnum fromType(String type) {
+ if (type == null || type.trim().isEmpty()) {
+ return null;
+ }
+
+ String trimmedType = type.trim();
+
+ for (FileTypeEnum fileType : values()) {
+ if (fileType.getCode().equalsIgnoreCase(trimmedType)
+ || fileType.getName().equals(trimmedType)) {
+ return fileType;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 根据文件后缀判断文件类型
+ */
+ public static FileTypeEnum fromSuffix(String suffix) {
+ if (suffix == null || suffix.trim().isEmpty()) {
+ return OTHER;
+ }
+
+ String normalizedSuffix = suffix.trim().replace(".", "").toLowerCase();
+ return EXTENSION_MAP.getOrDefault(normalizedSuffix, OTHER);
+ }
+
+ /**
+ * 根据文件名判断文件类型
+ */
+ public static FileTypeEnum fromFileName(String fileName) {
+ if (fileName == null || fileName.trim().isEmpty()) {
+ return OTHER;
+ }
+
+ int dotIndex = fileName.lastIndexOf('.');
+ if (dotIndex == -1) {
+ return OTHER;
+ }
+
+ String extension = fileName.substring(dotIndex + 1);
+ return fromSuffix(extension);
+ }
+
+ /**
+ * 根据分类获取所有类型
+ */
+ public static List getByCategory(FileCategory category) {
+ return CATEGORY_MAP.getOrDefault(category, Collections.emptyList());
+ }
+
+ /**
+ * 判断文件扩展名是否为媒体文件(视频或音频)
+ */
+ public static boolean isMediaFile(String extension) {
+ if (extension == null || extension.trim().isEmpty()) {
+ return false;
+ }
+ FileTypeEnum fileType = fromSuffix(extension);
+ return fileType == VIDEO || fileType == AUDIO;
+ }
+
+ /**
+ * 获取分类下所有后缀(用于前端筛选)
+ */
+ public static List getSuffixesByCategory(FileCategory category) {
+ return getByCategory(category).stream()
+ .filter(type -> type.getSuffixes() != null)
+ .flatMap(type -> type.getSuffixes().stream())
+ .distinct()
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 获取所有已知的文件后缀
+ */
+ public static List getAllKnownSuffixes() {
+ return Stream.of(values())
+ .filter(type -> type != OTHER && type.getSuffixes() != null)
+ .flatMap(type -> type.getSuffixes().stream())
+ .distinct()
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 判断是否为其他类型
+ */
+ public boolean isOther() {
+ return this == OTHER;
+ }
+
+ /**
+ * 判断某个后缀是否属于当前类型
+ */
+ public boolean containsSuffix(String suffix) {
+ if (suffix == null || this.suffixes == null) {
+ return false;
+ }
+ String normalizedSuffix = suffix.trim().replace(".", "").toLowerCase();
+ return this.suffixes.stream()
+ .anyMatch(s -> s.equalsIgnoreCase(normalizedSuffix));
+ }
+
+ /**
+ * 判断是否属于指定分类
+ */
+ public boolean isCategory(FileCategory category) {
+ return this.category == category;
+ }
+
+ /**
+ * 判断是否支持预览
+ */
+ public boolean isPreviewable() {
+ return Boolean.TRUE.equals(this.previewable);
+ }
+
+ /**
+ * 判断是否需要转换
+ */
+ public boolean isNeedConvert() {
+ return Boolean.TRUE.equals(this.needConvert);
+ }
+
+ /**
+ * 文件大类枚举
+ */
+ @Getter
+ public enum FileCategory {
+ IMAGE("image", "图片"),
+ VIDEO("video", "视频"),
+ AUDIO("audio", "音频"),
+ DOCUMENT("document", "文档"),
+ OTHER("other", "其他");
+
+ private final String code;
+ private final String name;
+
+ FileCategory(String code, String name) {
+ this.code = code;
+ this.name = name;
+ }
+
+ /**
+ * 根据code获取分类
+ */
+ public static FileCategory fromCode(String code) {
+ if (code == null) return null;
+ for (FileCategory category : values()) {
+ if (category.code.equalsIgnoreCase(code)) {
+ return category;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/fs-framework/fs-common-core/src/main/java/com/xddcodec/fs/framework/common/utils/FileUtils.java b/fs-framework/fs-common-core/src/main/java/com/xddcodec/fs/framework/common/utils/FileUtils.java
index a20763e4757842ab7ae427500698ec97d0bec5da..52a9311454bee2ca4a210d03ef6c5b93400667ad 100644
--- a/fs-framework/fs-common-core/src/main/java/com/xddcodec/fs/framework/common/utils/FileUtils.java
+++ b/fs-framework/fs-common-core/src/main/java/com/xddcodec/fs/framework/common/utils/FileUtils.java
@@ -3,7 +3,6 @@ package com.xddcodec.fs.framework.common.utils;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
-import com.xddcodec.fs.framework.common.constant.CommonConstant;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
@@ -20,75 +19,11 @@ import java.net.URLEncoder;
* @Date: 2024/6/7 11:12
*/
public class FileUtils {
+
private FileUtils() {
throw new IllegalStateException("Utility class");
}
- /**
- * 合法的后缀名,后续扩展
- */
- public static String[] ALLOWED_FILE_SUFFIX = new String[]{
- "png", "bmp", "jpg", "jpeg", "pdf",
- "xlsx", "xls", "gif", "svg", "txt",
- "zip", "ppt", "doc", "docx", "html",
- "htm", "ico", "mp3", "mp4", "java",
- "sql", "xml", "js", "py", "php", "vue",
- "sh", "cmd", "py3", "css", "md", "csv",
- "rar", "zip", "json"
- };
-
- public static String[] FILE_SUFFIX_IMAGE = new String[]{"png", "bmp", "jpg", "jpeg", "svg", "gif"};
-
- public static String[] FILE_SUFFIX_CODE = new String[]{"java", "sql", "js", "py", "py3", "php", "vue", "sh", "cmd", "css"};
-
- public static boolean isFileAllowed(String fileName) {
- for (String ext : ALLOWED_FILE_SUFFIX) {
- if (ext.equals(fileName)) {
- return true;
- }
- }
- return false;
- }
-
- public static boolean isFileAllowed(String fileName, String[] file) {
- for (String ext : file) {
- if (ext.equals(fileName)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * 是否为图片文件
- *
- * @param suffix
- * @return
- */
- public static boolean isImg(String suffix) {
- for (String ext : FILE_SUFFIX_IMAGE) {
- if (ext.equals(suffix)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * 是否是代码文件
- *
- * @param suffix
- * @return
- */
- public static boolean isCode(String suffix) {
- for (String ext : FILE_SUFFIX_CODE) {
- if (ext.equals(suffix)) {
- return true;
- }
- }
- return false;
- }
-
public static String getContentType(String filenameExtension) {
if (filenameExtension.equalsIgnoreCase(".pdf")) {
return "application/pdf";
diff --git a/fs-framework/fs-preview/pom.xml b/fs-framework/fs-preview/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0cd22e167fe438fb5b9a1352e52e500d1ba83c93
--- /dev/null
+++ b/fs-framework/fs-preview/pom.xml
@@ -0,0 +1,22 @@
+
+
+ 4.0.0
+
+ com.xddcodec.fs
+ fs-framework
+ ${revision}
+
+ fs-preview
+
+
+ fs-preview - 预览模块,提供文件预览支持
+
+
+
+
+ com.xddcodec.fs
+ fs-common-core
+
+
+
diff --git a/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/config/FilePreviewConfig.java b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/config/FilePreviewConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ce3db41e43e7e91da6b77bd81b5ccbceac31bde
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/config/FilePreviewConfig.java
@@ -0,0 +1,35 @@
+package com.xddcodec.fs.framework.preview.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 文件预览配置
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "fs.preview")
+public class FilePreviewConfig {
+
+ /**
+ * 预览文件最大大小(字节),默认500MB
+ */
+ private Long maxFileSize = 524288000L;
+
+ /**
+ * 单次Range请求最大大小(字节),默认10MB
+ */
+ private Long maxRangeSize = 10485760L;
+
+ /**
+ * 小文件直接传输阈值(字节),默认10MB
+ */
+ private Long smallFileSize = 10485760L;
+
+ /**
+ * 缓冲区大小(字节),默认8KB
+ */
+ private Integer bufferSize = 8192;
+}
+
diff --git a/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/core/PreviewContext.java b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/core/PreviewContext.java
new file mode 100644
index 0000000000000000000000000000000000000000..8610b2da62972d547d238e51c286f02ce58a3e0b
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/core/PreviewContext.java
@@ -0,0 +1,41 @@
+package com.xddcodec.fs.framework.preview.core;
+
+import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class PreviewContext {
+
+ /**
+ * 文件名
+ */
+ private String fileName;
+
+ /**
+ * 文件流路径
+ */
+ private String streamUrl;
+
+ /**
+ * 文件大小
+ */
+ private Long fileSize;
+
+ /**
+ * 文件扩展名
+ */
+ private String extension;
+
+ /**
+ * 预览类型
+ */
+ private FileTypeEnum fileType;
+
+ /**
+ * 是否需要转换
+ */
+ private Boolean needConvert;
+}
+
diff --git a/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/core/PreviewStrategy.java b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/core/PreviewStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc743828d69cbb189713b0b3dd453ab9a3b6cdb3
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/core/PreviewStrategy.java
@@ -0,0 +1,24 @@
+package com.xddcodec.fs.framework.preview.core;
+
+import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
+import org.springframework.ui.Model;
+
+public interface PreviewStrategy {
+
+ /**
+ * 是否支持该类型
+ */
+ boolean support(FileTypeEnum type);
+
+ /**
+ * 填充模板数据
+ */
+ void fillModel(PreviewContext context, Model model);
+
+ /**
+ * 优先级(数字越小优先级越高)
+ */
+ default int getPriority() {
+ return 100;
+ }
+}
diff --git a/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/factory/PreviewStrategyManager.java b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/factory/PreviewStrategyManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..15dcf93de1ddf37beeace6bcc8ea4bc083eab861
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/factory/PreviewStrategyManager.java
@@ -0,0 +1,54 @@
+package com.xddcodec.fs.framework.preview.factory;
+
+import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
+import com.xddcodec.fs.framework.preview.core.PreviewStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * 预览策略管理器
+ */
+@Slf4j
+@Component
+public class PreviewStrategyManager {
+
+ private final List strategies;
+ private final PreviewStrategy defaultStrategy;
+
+ public PreviewStrategyManager(List strategies) {
+ this.strategies = strategies.stream()
+ .sorted(Comparator.comparingInt(PreviewStrategy::getPriority))
+ .toList();
+
+ // 找到兜底策略(UnsupportedPreviewStrategy,优先级最低)
+ this.defaultStrategy = strategies.stream()
+ .max(Comparator.comparingInt(PreviewStrategy::getPriority))
+ .orElse(null);
+
+ log.info("加载 {} 个预览策略", strategies.size());
+ if (defaultStrategy != null) {
+ log.info("默认策略: {}", defaultStrategy.getClass().getSimpleName());
+ }
+ }
+
+ /**
+ * 获取预览策略
+ * 注意:此方法不应该抛异常,因为Service层已经检查过isPreviewable()
+ */
+ public PreviewStrategy getStrategy(FileTypeEnum fileType) {
+ PreviewStrategy strategy = strategies.stream()
+ .filter(s -> s.support(fileType))
+ .findFirst()
+ .orElse(defaultStrategy);
+
+ if (strategy == null) {
+ log.error("找不到预览策略: fileType={}", fileType.getName());
+ throw new IllegalStateException("预览策略未正确初始化");
+ }
+
+ return strategy;
+ }
+}
diff --git a/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/AbstractPreviewStrategy.java b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/AbstractPreviewStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..ec7cd6c06c442748146860dfb51124b126747752
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/AbstractPreviewStrategy.java
@@ -0,0 +1,35 @@
+package com.xddcodec.fs.framework.preview.strategy;
+
+import com.xddcodec.fs.framework.preview.core.PreviewContext;
+import com.xddcodec.fs.framework.preview.core.PreviewStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ui.Model;
+
+
+/**
+ * 抽象预览策略(提供通用功能)
+ */
+@Slf4j
+public abstract class AbstractPreviewStrategy implements PreviewStrategy {
+
+ @Override
+ public void fillModel(PreviewContext context, Model model) {
+ // 填充基础数据
+ model.addAttribute("fileName", context.getFileName());
+ model.addAttribute("fileSize", context.getFileSize());
+ model.addAttribute("fileType", context.getFileType().getName());
+ model.addAttribute("extension", context.getExtension());
+ model.addAttribute("streamUrl", context.getStreamUrl());
+
+ // 子类填充特定数据
+ fillSpecificModel(context, model);
+
+ log.info("策略 [{}] 填充完成", this.getClass().getSimpleName());
+ }
+
+ /**
+ * 子类实现:填充特定数据
+ */
+ protected abstract void fillSpecificModel(PreviewContext context, Model model);
+}
+
diff --git a/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/AudioPreviewStrategy.java b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/AudioPreviewStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..30d1b3f35bf8399e5546f6cb52dc73e92a6c715a
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/AudioPreviewStrategy.java
@@ -0,0 +1,35 @@
+package com.xddcodec.fs.framework.preview.strategy.impl;
+
+import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
+import com.xddcodec.fs.framework.preview.core.PreviewContext;
+import com.xddcodec.fs.framework.preview.strategy.AbstractPreviewStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.ui.Model;
+
+/**
+ * 音频预览策略
+ */
+@Slf4j
+@Component
+public class AudioPreviewStrategy extends AbstractPreviewStrategy {
+
+ @Override
+ public boolean support(FileTypeEnum fileType) {
+ return fileType == FileTypeEnum.AUDIO;
+ }
+
+ @Override
+ protected void fillSpecificModel(PreviewContext context, Model model) {
+ log.info("音频预览策略填充完成 - 文件名: {}, 格式: {}, 大小: {}",
+ context.getFileName(),
+ context.getExtension(),
+ context.getFileSize());
+ }
+
+ @Override
+ public int getPriority() {
+ return 16;
+ }
+}
+
diff --git a/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/CodePreviewStrategy.java b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/CodePreviewStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..d9a28c1483eb1f14ba14c1152f085942076b976a
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/CodePreviewStrategy.java
@@ -0,0 +1,54 @@
+package com.xddcodec.fs.framework.preview.strategy.impl;
+
+import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
+import com.xddcodec.fs.framework.preview.core.PreviewContext;
+import com.xddcodec.fs.framework.preview.strategy.AbstractPreviewStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.ui.Model;
+
+import java.util.Map;
+
+@Slf4j
+@Component
+public class CodePreviewStrategy extends AbstractPreviewStrategy {
+ private static final Map LANGUAGE_MAP = Map.ofEntries(
+ Map.entry("java", "java"),
+ Map.entry("js", "javascript"),
+ Map.entry("jsx", "javascript"),
+ Map.entry("ts", "typescript"),
+ Map.entry("tsx", "typescript"),
+ Map.entry("py", "python"),
+ Map.entry("html", "html"),
+ Map.entry("htm", "html"),
+ Map.entry("css", "css"),
+ Map.entry("scss", "css"),
+ Map.entry("less", "css"),
+ Map.entry("sql", "sql"),
+ Map.entry("json", "json"),
+ Map.entry("xml", "xml"),
+ Map.entry("yaml", "yaml"),
+ Map.entry("yml", "yaml"),
+ Map.entry("md", "markdown.html")
+ );
+
+ @Override
+ public boolean support(FileTypeEnum fileType) {
+ return fileType == FileTypeEnum.CODE;
+ }
+
+ @Override
+ protected void fillSpecificModel(PreviewContext context, Model model) {
+ String language = LANGUAGE_MAP.getOrDefault(
+ context.getExtension() == null ? "" : context.getExtension().toLowerCase(),
+ "plaintext"
+ );
+ model.addAttribute("language", language);
+ }
+
+
+ @Override
+ public int getPriority() {
+ return 30;
+ }
+}
diff --git a/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/ImagePreviewStrategy.java b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/ImagePreviewStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..98b28e3f80269c590abacb4ad611ea8c4f61c252
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/ImagePreviewStrategy.java
@@ -0,0 +1,39 @@
+package com.xddcodec.fs.framework.preview.strategy.impl;
+
+import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
+import com.xddcodec.fs.framework.preview.core.PreviewContext;
+import com.xddcodec.fs.framework.preview.strategy.AbstractPreviewStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.ui.Model;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
+/**
+ * 图片预览策略
+ */
+@Slf4j
+@Component
+public class ImagePreviewStrategy extends AbstractPreviewStrategy {
+
+ @Override
+ public boolean support(FileTypeEnum fileType) {
+ return fileType == FileTypeEnum.IMAGE;
+ }
+
+ @Override
+ protected void fillSpecificModel(PreviewContext context, Model model) {
+ log.info("图片预览策略填充完成 - 文件名: {}, 格式: {}, 大小: {}",
+ context.getFileName(),
+ context.getExtension(),
+ context.getFileSize());
+ }
+
+ @Override
+ public int getPriority() {
+ return 10;
+ }
+
+}
diff --git a/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/MarkdownPreviewStrategy.java b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/MarkdownPreviewStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..7fd8ea08bb66c886b86f0e3bf274883ffcbea7de
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/MarkdownPreviewStrategy.java
@@ -0,0 +1,33 @@
+package com.xddcodec.fs.framework.preview.strategy.impl;
+
+import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
+import com.xddcodec.fs.framework.preview.core.PreviewContext;
+import com.xddcodec.fs.framework.preview.strategy.AbstractPreviewStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.ui.Model;
+
+/**
+ * Markdown 预览策略
+ * 支持 .md 和 .markdown 文件的在线预览
+ */
+@Slf4j
+@Component
+public class MarkdownPreviewStrategy extends AbstractPreviewStrategy {
+
+ @Override
+ public boolean support(FileTypeEnum fileType) {
+ return fileType == FileTypeEnum.MARKDOWN;
+ }
+
+ @Override
+ protected void fillSpecificModel(PreviewContext context, Model model) {
+ log.info("Markdown 预览策略填充完成 - 文件名: {}, 大小: {} bytes",
+ context.getFileName(), context.getFileSize());
+ }
+
+ @Override
+ public int getPriority() {
+ return 25;
+ }
+}
diff --git a/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/PdfPreviewStrategy.java b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/PdfPreviewStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..87011b1a241413fdc1438d517cdfbff6eb290c08
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/PdfPreviewStrategy.java
@@ -0,0 +1,50 @@
+package com.xddcodec.fs.framework.preview.strategy.impl;
+
+import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
+import com.xddcodec.fs.framework.preview.core.PreviewContext;
+import com.xddcodec.fs.framework.preview.strategy.AbstractPreviewStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.ui.Model;
+
+/**
+ * ✅ PDF预览策略
+ */
+@Slf4j
+@Component
+public class PdfPreviewStrategy extends AbstractPreviewStrategy {
+
+ @Override
+ public boolean support(FileTypeEnum fileType) {
+ // 支持 PDF 本身,以及需要转换为 PDF 的类型(Word/Excel/PPT)
+ return fileType == FileTypeEnum.PDF
+ || fileType == FileTypeEnum.WORD
+ || fileType == FileTypeEnum.EXCEL
+ || fileType == FileTypeEnum.PPT;
+ }
+
+ @Override
+ protected void fillSpecificModel(PreviewContext context, Model model) {
+ FileTypeEnum fileType = context.getFileType();
+
+ Boolean needConvert = context.getNeedConvert();
+ if (needConvert != null && needConvert) {
+ model.addAttribute("needConvert", true);
+ model.addAttribute("originalType", fileType.getName());
+ model.addAttribute("convertStatus", "pending");
+
+ log.info("Office 文档需要转换 - 文件: {}, 类型: {} -> PDF",
+ context.getFileName(), fileType.getName());
+ } else {
+ model.addAttribute("needConvert", false);
+ log.info("PDF 文档直接预览 - 文件: {}", context.getFileName());
+ }
+
+ log.info("PDF 预览策略填充完成 - 文件名: {}", context.getFileName());
+ }
+
+ @Override
+ public int getPriority() {
+ return 20;
+ }
+}
diff --git a/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/UnsupportedPreviewStrategy.java b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/UnsupportedPreviewStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..1f373965fdb57200d6ea9f4a8978c00179550283
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/UnsupportedPreviewStrategy.java
@@ -0,0 +1,34 @@
+package com.xddcodec.fs.framework.preview.strategy.impl;
+
+import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
+import com.xddcodec.fs.framework.preview.core.PreviewContext;
+import com.xddcodec.fs.framework.preview.strategy.AbstractPreviewStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.ui.Model;
+
+/**
+ * 不支持预览的策略(兜底)
+ */
+@Slf4j
+@Component
+public class UnsupportedPreviewStrategy extends AbstractPreviewStrategy {
+
+ @Override
+ public boolean support(FileTypeEnum fileType) {
+ return !fileType.isPreviewable();
+ }
+
+ @Override
+ protected void fillSpecificModel(PreviewContext context, Model model) {
+ log.warn("不支持预览的文件类型: {}", context.getFileType().getName());
+
+ model.addAttribute("message", "该文件类型暂不支持在线预览");
+ model.addAttribute("supportDownload", true);
+ }
+
+ @Override
+ public int getPriority() {
+ return 999; // 最低优先级(兜底)
+ }
+}
diff --git a/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/VideoPreviewStrategy.java b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/VideoPreviewStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..f4bcc6dafb0ff529544035592986806a0fc29099
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/java/com/xddcodec/fs/framework/preview/strategy/impl/VideoPreviewStrategy.java
@@ -0,0 +1,35 @@
+package com.xddcodec.fs.framework.preview.strategy.impl;
+
+import com.xddcodec.fs.framework.common.enums.FileTypeEnum;
+import com.xddcodec.fs.framework.preview.core.PreviewContext;
+import com.xddcodec.fs.framework.preview.strategy.AbstractPreviewStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.ui.Model;
+
+/**
+ * 视频预览策略
+ */
+@Slf4j
+@Component
+public class VideoPreviewStrategy extends AbstractPreviewStrategy {
+
+ @Override
+ public boolean support(FileTypeEnum fileType) {
+ return fileType == FileTypeEnum.VIDEO;
+ }
+
+ @Override
+ protected void fillSpecificModel(PreviewContext context, Model model) {
+ log.info("视频预览策略填充完成 - 文件名: {}, 格式: {}, 大小: {}",
+ context.getFileName(),
+ context.getExtension(),
+ context.getFileSize());
+ }
+
+ @Override
+ public int getPriority() {
+ return 15;
+ }
+}
+
diff --git a/fs-framework/fs-preview/src/main/resources/static/lib/marked/highlight.min.js b/fs-framework/fs-preview/src/main/resources/static/lib/marked/highlight.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..4a9444ef55c9f535a4f0a6721415b3ee6a240df6
--- /dev/null
+++ b/fs-framework/fs-preview/src/main/resources/static/lib/marked/highlight.min.js
@@ -0,0 +1 @@
+var hljs=new function(){function k(v){return v.replace(/&/gm,"&").replace(//gm,">")}function t(v){return v.nodeName.toLowerCase()}function i(w,x){var v=w&&w.exec(x);return v&&v.index==0}function d(v){return Array.prototype.map.call(v.childNodes,function(w){if(w.nodeType==3){return b.useBR?w.nodeValue.replace(/\n/g,""):w.nodeValue}if(t(w)=="br"){return"\n"}return d(w)}).join("")}function r(w){var v=(w.className+" "+(w.parentNode?w.parentNode.className:"")).split(/\s+/);v=v.map(function(x){return x.replace(/^language-/,"")});return v.filter(function(x){return j(x)||x=="no-highlight"})[0]}function o(x,y){var v={};for(var w in x){v[w]=x[w]}if(y){for(var w in y){v[w]=y[w]}}return v}function u(x){var v=[];(function w(y,z){for(var A=y.firstChild;A;A=A.nextSibling){if(A.nodeType==3){z+=A.nodeValue.length}else{if(t(A)=="br"){z+=1}else{if(A.nodeType==1){v.push({event:"start",offset:z,node:A});z=w(A,z);v.push({event:"stop",offset:z,node:A})}}}}return z})(x,0);return v}function q(w,y,C){var x=0;var F="";var z=[];function B(){if(!w.length||!y.length){return w.length?w:y}if(w[0].offset!=y[0].offset){return(w[0].offset"}function E(G){F+=""+t(G)+">"}function v(G){(G.event=="start"?A:E)(G.node)}while(w.length||y.length){var D=B();F+=k(C.substr(x,D[0].offset-x));x=D[0].offset;if(D==w){z.reverse().forEach(E);do{v(D.splice(0,1)[0]);D=B()}while(D==w&&D.length&&D[0].offset==x);z.reverse().forEach(A)}else{if(D[0].event=="start"){z.push(D[0].node)}else{z.pop()}v(D.splice(0,1)[0])}}return F+k(C.substr(x))}function m(y){function v(z){return(z&&z.source)||z}function w(A,z){return RegExp(v(A),"m"+(y.cI?"i":"")+(z?"g":""))}function x(D,C){if(D.compiled){return}D.compiled=true;D.k=D.k||D.bK;if(D.k){var z={};function E(G,F){if(y.cI){F=F.toLowerCase()}F.split(" ").forEach(function(H){var I=H.split("|");z[I[0]]=[G,I[1]?Number(I[1]):1]})}if(typeof D.k=="string"){E("keyword",D.k)}else{Object.keys(D.k).forEach(function(F){E(F,D.k[F])})}D.k=z}D.lR=w(D.l||/\b[A-Za-z0-9_]+\b/,true);if(C){if(D.bK){D.b=D.bK.split(" ").join("|")}if(!D.b){D.b=/\B|\b/}D.bR=w(D.b);if(!D.e&&!D.eW){D.e=/\B|\b/}if(D.e){D.eR=w(D.e)}D.tE=v(D.e)||"";if(D.eW&&C.tE){D.tE+=(D.e?"|":"")+C.tE}}if(D.i){D.iR=w(D.i)}if(D.r===undefined){D.r=1}if(!D.c){D.c=[]}var B=[];D.c.forEach(function(F){if(F.v){F.v.forEach(function(G){B.push(o(F,G))})}else{B.push(F=="self"?D:F)}});D.c=B;D.c.forEach(function(F){x(F,D)});if(D.starts){x(D.starts,C)}var A=D.c.map(function(F){return F.bK?"\\.?\\b("+F.b+")\\b\\.?":F.b}).concat([D.tE]).concat([D.i]).map(v).filter(Boolean);D.t=A.length?w(A.join("|"),true):{exec:function(F){return null}};D.continuation={}}x(y)}function c(S,L,J,R){function v(U,V){for(var T=0;T";U+=Z+'">';return U+X+Y}function N(){var U=k(C);if(!I.k){return U}var T="";var X=0;I.lR.lastIndex=0;var V=I.lR.exec(U);while(V){T+=U.substr(X,V.index-X);var W=E(I,V);if(W){H+=W[1];T+=w(W[0],V[0])}else{T+=V[0]}X=I.lR.lastIndex;V=I.lR.exec(U)}return T+U.substr(X)}function F(){if(I.sL&&!f[I.sL]){return k(C)}var T=I.sL?c(I.sL,C,true,I.continuation.top):g(C);if(I.r>0){H+=T.r}if(I.subLanguageMode=="continuous"){I.continuation.top=T.top}return w(T.language,T.value,false,true)}function Q(){return I.sL!==undefined?F():N()}function P(V,U){var T=V.cN?w(V.cN,"",true):"";if(V.rB){D+=T;C=""}else{if(V.eB){D+=k(U)+T;C=""}else{D+=T;C=U}}I=Object.create(V,{parent:{value:I}})}function G(T,X){C+=T;if(X===undefined){D+=Q();return 0}var V=v(X,I);if(V){D+=Q();P(V,X);return V.rB?0:X.length}var W=z(I,X);if(W){var U=I;if(!(U.rE||U.eE)){C+=X}D+=Q();do{if(I.cN){D+=""}H+=I.r;I=I.parent}while(I!=W.parent);if(U.eE){D+=k(X)}C="";if(W.starts){P(W.starts,"")}return U.rE?0:X.length}if(A(X,I)){throw new Error('Illegal lexeme "'+X+'" for mode "'+(I.cN||"")+'"')}C+=X;return X.length||1}var M=j(S);if(!M){throw new Error('Unknown language: "'+S+'"')}m(M);var I=R||M;var D="";for(var K=I;K!=M;K=K.parent){if(K.cN){D=w(K.cN,D,true)}}var C="";var H=0;try{var B,y,x=0;while(true){I.t.lastIndex=x;B=I.t.exec(L);if(!B){break}y=G(L.substr(x,B.index-x),B[0]);x=B.index+y}G(L.substr(x));for(var K=I;K.parent;K=K.parent){if(K.cN){D+=""}}return{r:H,value:D,language:S,top:I}}catch(O){if(O.message.indexOf("Illegal")!=-1){return{r:0,value:k(L)}}else{throw O}}}function g(y,x){x=x||b.languages||Object.keys(f);var v={r:0,value:k(y)};var w=v;x.forEach(function(z){if(!j(z)){return}var A=c(z,y,false);A.language=z;if(A.r>w.r){w=A}if(A.r>v.r){w=v;v=A}});if(w.language){v.second_best=w}return v}function h(v){if(b.tabReplace){v=v.replace(/^((<[^>]+>|\t)+)/gm,function(w,z,y,x){return z.replace(/\t/g,b.tabReplace)})}if(b.useBR){v=v.replace(/\n/g,"
")}return v}function p(z){var y=d(z);var A=r(z);if(A=="no-highlight"){return}var v=A?c(A,y,true):g(y);var w=u(z);if(w.length){var x=document.createElementNS("http://www.w3.org/1999/xhtml","pre");x.innerHTML=v.value;v.value=q(w,u(x),y)}v.value=h(v.value);z.innerHTML=v.value;z.className+=" hljs "+(!A&&v.language||"");z.result={language:v.language,re:v.r};if(v.second_best){z.second_best={language:v.second_best.language,re:v.second_best.r}}}var b={classPrefix:"hljs-",tabReplace:null,useBR:false,languages:undefined};function s(v){b=o(b,v)}function l(){if(l.called){return}l.called=true;var v=document.querySelectorAll("pre code");Array.prototype.forEach.call(v,p)}function a(){addEventListener("DOMContentLoaded",l,false);addEventListener("load",l,false)}var f={};var n={};function e(v,x){var w=f[v]=x(this);if(w.aliases){w.aliases.forEach(function(y){n[y]=v})}}function j(v){return f[v]||f[n[v]]}this.highlight=c;this.highlightAuto=g;this.fixMarkup=h;this.highlightBlock=p;this.configure=s;this.initHighlighting=l;this.initHighlightingOnLoad=a;this.registerLanguage=e;this.getLanguage=j;this.inherit=o;this.IR="[a-zA-Z][a-zA-Z0-9_]*";this.UIR="[a-zA-Z_][a-zA-Z0-9_]*";this.NR="\\b\\d+(\\.\\d+)?";this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";this.BNR="\\b(0b[01]+)";this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\[\\s\\S]",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE]};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE]};this.CLCM={cN:"comment",b:"//",e:"$"};this.CBLCLM={cN:"comment",b:"/\\*",e:"\\*/"};this.HCM={cN:"comment",b:"#",e:"$"};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.REGEXP_MODE={cN:"regexp",b:/\//,e:/\/[gim]*/,i:/\n/,c:[this.BE,{b:/\[/,e:/\]/,r:0,c:[this.BE]}]};this.TM={cN:"title",b:this.IR,r:0};this.UTM={cN:"title",b:this.UIR,r:0}}();hljs.registerLanguage("ruleslanguage",function(a){return{k:{keyword:"BILL_PERIOD BILL_START BILL_STOP RS_EFFECTIVE_START RS_EFFECTIVE_STOP RS_JURIS_CODE RS_OPCO_CODE INTDADDATTRIBUTE|5 INTDADDVMSG|5 INTDBLOCKOP|5 INTDBLOCKOPNA|5 INTDCLOSE|5 INTDCOUNT|5 INTDCOUNTSTATUSCODE|5 INTDCREATEMASK|5 INTDCREATEDAYMASK|5 INTDCREATEFACTORMASK|5 INTDCREATEHANDLE|5 INTDCREATEOVERRIDEDAYMASK|5 INTDCREATEOVERRIDEMASK|5 INTDCREATESTATUSCODEMASK|5 INTDCREATETOUPERIOD|5 INTDDELETE|5 INTDDIPTEST|5 INTDEXPORT|5 INTDGETERRORCODE|5 INTDGETERRORMESSAGE|5 INTDISEQUAL|5 INTDJOIN|5 INTDLOAD|5 INTDLOADACTUALCUT|5 INTDLOADDATES|5 INTDLOADHIST|5 INTDLOADLIST|5 INTDLOADLISTDATES|5 INTDLOADLISTENERGY|5 INTDLOADLISTHIST|5 INTDLOADRELATEDCHANNEL|5 INTDLOADSP|5 INTDLOADSTAGING|5 INTDLOADUOM|5 INTDLOADUOMDATES|5 INTDLOADUOMHIST|5 INTDLOADVERSION|5 INTDOPEN|5 INTDREADFIRST|5 INTDREADNEXT|5 INTDRECCOUNT|5 INTDRELEASE|5 INTDREPLACE|5 INTDROLLAVG|5 INTDROLLPEAK|5 INTDSCALAROP|5 INTDSCALE|5 INTDSETATTRIBUTE|5 INTDSETDSTPARTICIPANT|5 INTDSETSTRING|5 INTDSETVALUE|5 INTDSETVALUESTATUS|5 INTDSHIFTSTARTTIME|5 INTDSMOOTH|5 INTDSORT|5 INTDSPIKETEST|5 INTDSUBSET|5 INTDTOU|5 INTDTOURELEASE|5 INTDTOUVALUE|5 INTDUPDATESTATS|5 INTDVALUE|5 STDEV INTDDELETEEX|5 INTDLOADEXACTUAL|5 INTDLOADEXCUT|5 INTDLOADEXDATES|5 INTDLOADEX|5 INTDLOADEXRELATEDCHANNEL|5 INTDSAVEEX|5 MVLOAD|5 MVLOADACCT|5 MVLOADACCTDATES|5 MVLOADACCTHIST|5 MVLOADDATES|5 MVLOADHIST|5 MVLOADLIST|5 MVLOADLISTDATES|5 MVLOADLISTHIST|5 IF FOR NEXT DONE SELECT END CALL ABORT CLEAR CHANNEL FACTOR LIST NUMBER OVERRIDE SET WEEK DISTRIBUTIONNODE ELSE WHEN THEN OTHERWISE IENUM CSV INCLUDE LEAVE RIDER SAVE DELETE NOVALUE SECTION WARN SAVE_UPDATE DETERMINANT LABEL REPORT REVENUE EACH IN FROM TOTAL CHARGE BLOCK AND OR CSV_FILE RATE_CODE AUXILIARY_DEMAND UIDACCOUNT RS BILL_PERIOD_SELECT HOURS_PER_MONTH INTD_ERROR_STOP SEASON_SCHEDULE_NAME ACCOUNTFACTOR ARRAYUPPERBOUND CALLSTOREDPROC GETADOCONNECTION GETCONNECT GETDATASOURCE GETQUALIFIER GETUSERID HASVALUE LISTCOUNT LISTOP LISTUPDATE LISTVALUE PRORATEFACTOR RSPRORATE SETBINPATH SETDBMONITOR WQ_OPEN BILLINGHOURS DATE DATEFROMFLOAT DATETIMEFROMSTRING DATETIMETOSTRING DATETOFLOAT DAY DAYDIFF DAYNAME DBDATETIME HOUR MINUTE MONTH MONTHDIFF MONTHHOURS MONTHNAME ROUNDDATE SAMEWEEKDAYLASTYEAR SECOND WEEKDAY WEEKDIFF YEAR YEARDAY YEARSTR COMPSUM HISTCOUNT HISTMAX HISTMIN HISTMINNZ HISTVALUE MAXNRANGE MAXRANGE MINRANGE COMPIKVA COMPKVA COMPKVARFROMKQKW COMPLF IDATTR FLAG LF2KW LF2KWH MAXKW POWERFACTOR READING2USAGE AVGSEASON MAXSEASON MONTHLYMERGE SEASONVALUE SUMSEASON ACCTREADDATES ACCTTABLELOAD CONFIGADD CONFIGGET CREATEOBJECT CREATEREPORT EMAILCLIENT EXPBLKMDMUSAGE EXPMDMUSAGE EXPORT_USAGE FACTORINEFFECT GETUSERSPECIFIEDSTOP INEFFECT ISHOLIDAY RUNRATE SAVE_PROFILE SETREPORTTITLE USEREXIT WATFORRUNRATE TO TABLE ACOS ASIN ATAN ATAN2 BITAND CEIL COS COSECANT COSH COTANGENT DIVQUOT DIVREM EXP FABS FLOOR FMOD FREPM FREXPN LOG LOG10 MAX MAXN MIN MINNZ MODF POW ROUND ROUND2VALUE ROUNDINT SECANT SIN SINH SQROOT TAN TANH FLOAT2STRING FLOAT2STRINGNC INSTR LEFT LEN LTRIM MID RIGHT RTRIM STRING STRINGNC TOLOWER TOUPPER TRIM NUMDAYS READ_DATE STAGING",built_in:"IDENTIFIER OPTIONS XML_ELEMENT XML_OP XML_ELEMENT_OF DOMDOCCREATE DOMDOCLOADFILE DOMDOCLOADXML DOMDOCSAVEFILE DOMDOCGETROOT DOMDOCADDPI DOMNODEGETNAME DOMNODEGETTYPE DOMNODEGETVALUE DOMNODEGETCHILDCT DOMNODEGETFIRSTCHILD DOMNODEGETSIBLING DOMNODECREATECHILDELEMENT DOMNODESETATTRIBUTE DOMNODEGETCHILDELEMENTCT DOMNODEGETFIRSTCHILDELEMENT DOMNODEGETSIBLINGELEMENT DOMNODEGETATTRIBUTECT DOMNODEGETATTRIBUTEI DOMNODEGETATTRIBUTEBYNAME DOMNODEGETBYNAME"},c:[a.CLCM,a.CBLCLM,a.ASM,a.QSM,a.CNM,{cN:"array",b:"#[a-zA-Z .]+"}]}});hljs.registerLanguage("ruby",function(e){var h="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?";var g="and false then defined module in return redo if BEGIN retry end for true self when next until do begin unless END rescue nil else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor";var a={cN:"yardoctag",b:"@[A-Za-z]+"};var i={cN:"comment",v:[{b:"#",e:"$",c:[a]},{b:"^\\=begin",e:"^\\=end",c:[a],r:10},{b:"^__END__",e:"\\n$"}]};var c={cN:"subst",b:"#\\{",e:"}",k:g};var d={cN:"string",c:[e.BE,c],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:"%[qw]?\\(",e:"\\)"},{b:"%[qw]?\\[",e:"\\]"},{b:"%[qw]?{",e:"}"},{b:"%[qw]?<",e:">",r:10},{b:"%[qw]?/",e:"/",r:10},{b:"%[qw]?%",e:"%",r:10},{b:"%[qw]?-",e:"-",r:10},{b:"%[qw]?\\|",e:"\\|",r:10},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]};var b={cN:"params",b:"\\(",e:"\\)",k:g};var f=[d,i,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{cN:"inheritance",b:"<\\s*",c:[{cN:"parent",b:"("+e.IR+"::)?"+e.IR}]},i]},{cN:"function",bK:"def",e:" |$|;",r:0,c:[e.inherit(e.TM,{b:h}),b,i]},{cN:"constant",b:"(::)?(\\b[A-Z]\\w*(::)?)+",r:0},{cN:"symbol",b:":",c:[d,{b:h}],r:0},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{cN:"variable",b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{b:"("+e.RSR+")\\s*",c:[i,{cN:"regexp",c:[e.BE,c],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}],r:0}];c.c=f;b.c=f;return{k:g,c:f}});hljs.registerLanguage("haml",function(a){return{cI:true,c:[{cN:"doctype",b:"^!!!( (5|1\\.1|Strict|Frameset|Basic|Mobile|RDFa|XML\\b.*))?$",r:10},{cN:"comment",b:"^\\s*(!=#|=#|-#|/).*$",r:0},{b:"^\\s*(-|=|!=)(?!#)",starts:{e:"\\n",sL:"ruby"}},{cN:"tag",b:"^\\s*%",c:[{cN:"title",b:"\\w+"},{cN:"value",b:"[#\\.]\\w+"},{b:"{\\s*",e:"\\s*}",eE:true,c:[{b:":\\w+\\s*=>",e:",\\s+",rB:true,eW:true,c:[{cN:"symbol",b:":\\w+"},{cN:"string",b:'"',e:'"'},{cN:"string",b:"'",e:"'"},{b:"\\w+",r:0}]}]},{b:"\\(\\s*",e:"\\s*\\)",eE:true,c:[{b:"\\w+\\s*=",e:"\\s+",rB:true,eW:true,c:[{cN:"attribute",b:"\\w+",r:0},{cN:"string",b:'"',e:'"'},{cN:"string",b:"'",e:"'"},{b:"\\w+",r:0}]},]}]},{cN:"bullet",b:"^\\s*[=~]\\s*",r:0},{b:"#{",starts:{e:"}",sL:"ruby"}}]}});hljs.registerLanguage("haskell",function(f){var g={cN:"comment",v:[{b:"--",e:"$"},{b:"{-",e:"-}",c:["self"]}]};var e={cN:"pragma",b:"{-#",e:"#-}"};var b={cN:"preprocessor",b:"^#",e:"$"};var d={cN:"type",b:"\\b[A-Z][\\w']*",r:0};var c={cN:"container",b:"\\(",e:"\\)",i:'"',c:[e,g,b,{cN:"type",b:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},f.inherit(f.TM,{b:"[_a-z][\\w']*"})]};var a={cN:"container",b:"{",e:"}",c:c.c};return{k:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",c:[{cN:"module",b:"\\bmodule\\b",e:"where",k:"module where",c:[c,g],i:"\\W\\.|;"},{cN:"import",b:"\\bimport\\b",e:"$",k:"import|0 qualified as hiding",c:[c,g],i:"\\W\\.|;"},{cN:"class",b:"^(\\s*)?(class|instance)\\b",e:"where",k:"class family instance where",c:[d,c,g]},{cN:"typedef",b:"\\b(data|(new)?type)\\b",e:"$",k:"data family type newtype deriving",c:[e,g,d,c,a]},{cN:"default",bK:"default",e:"$",c:[d,c,g]},{cN:"infix",bK:"infix infixl infixr",e:"$",c:[f.CNM,g]},{cN:"foreign",b:"\\bforeign\\b",e:"$",k:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",c:[d,f.QSM,g]},{cN:"shebang",b:"#!\\/usr\\/bin\\/env runhaskell",e:"$"},e,g,b,f.QSM,f.CNM,d,f.inherit(f.TM,{b:"^[_a-z][\\w']*"}),{b:"->|<-"}]}});hljs.registerLanguage("xml",function(a){var c="[A-Za-z0-9\\._:-]+";var d={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php",subLanguageMode:"continuous"};var b={eW:true,i:/,r:0,c:[d,{cN:"attribute",b:c,r:0},{b:"=",r:0,c:[{cN:"value",v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s\/>]+/}]}]}]};return{aliases:["html"],cI:true,c:[{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"",rE:true,sL:"css"}},{cN:"tag",b:"