3 namespace BookStack\Exports\ZipExports;
5 use BookStack\App\Model;
6 use BookStack\Entities\Queries\EntityQueries;
7 use BookStack\References\ModelResolvers\AttachmentModelResolver;
8 use BookStack\References\ModelResolvers\BookLinkModelResolver;
9 use BookStack\References\ModelResolvers\ChapterLinkModelResolver;
10 use BookStack\References\ModelResolvers\CrossLinkModelResolver;
11 use BookStack\References\ModelResolvers\ImageModelResolver;
12 use BookStack\References\ModelResolvers\PageLinkModelResolver;
13 use BookStack\References\ModelResolvers\PagePermalinkModelResolver;
14 use BookStack\Uploads\ImageStorage;
16 class ZipReferenceParser
19 * @var CrossLinkModelResolver[]|null
21 protected ?array $modelResolvers = null;
23 public function __construct(
24 protected EntityQueries $queries
29 * Parse and replace references in the given content.
30 * Calls the handler for each model link detected and replaces the link
31 * with the handler return value if provided.
32 * Returns the resulting content with links replaced.
33 * @param callable(Model):(string|null) $handler
35 public function parseLinks(string $content, callable $handler): string
37 $linkRegex = $this->getLinkRegex();
39 preg_match_all($linkRegex, $content, $matches);
41 if (count($matches) < 2) {
45 foreach ($matches[1] as $link) {
46 $model = $this->linkToModel($link);
48 $result = $handler($model);
49 if ($result !== null) {
50 $content = str_replace($link, $result, $content);
59 * Parse and replace references in the given content.
60 * Calls the handler for each reference detected and replaces the link
61 * with the handler return value if provided.
62 * Returns the resulting content string with references replaced.
63 * @param callable(string $type, int $id):(string|null) $handler
65 public function parseReferences(string $content, callable $handler): string
67 $referenceRegex = '/\[\[bsexport:([a-z]+):(\d+)]]/';
69 preg_match_all($referenceRegex, $content, $matches);
71 if (count($matches) < 3) {
75 for ($i = 0; $i < count($matches[0]); $i++) {
76 $referenceText = $matches[0][$i];
77 $type = strtolower($matches[1][$i]);
78 $id = intval($matches[2][$i]);
79 $result = $handler($type, $id);
80 if ($result !== null) {
81 $content = str_replace($referenceText, $result, $content);
90 * Attempt to resolve the given link to a model using the instance model resolvers.
92 protected function linkToModel(string $link): ?Model
94 foreach ($this->getModelResolvers() as $resolver) {
95 $model = $resolver->resolve($link);
96 if (!is_null($model)) {
104 protected function getModelResolvers(): array
106 if (isset($this->modelResolvers)) {
107 return $this->modelResolvers;
110 $this->modelResolvers = [
111 new PagePermalinkModelResolver($this->queries->pages),
112 new PageLinkModelResolver($this->queries->pages),
113 new ChapterLinkModelResolver($this->queries->chapters),
114 new BookLinkModelResolver($this->queries->books),
115 new ImageModelResolver(),
116 new AttachmentModelResolver(),
119 return $this->modelResolvers;
123 * Build the regex to identify links we should handle in content.
125 protected function getLinkRegex(): string
127 $urls = [rtrim(url('/'), '/')];
128 $imageUrl = rtrim(ImageStorage::getPublicUrl('/'), '/');
129 if ($urls[0] !== $imageUrl) {
134 $urlBaseRegex = implode('|', array_map(function ($url) {
135 return preg_quote($url, '/');
138 return "/(({$urlBaseRegex}).*?)[\\t\\n\\f>\"'=?#()]/";