Skip to content

Plugin Core

This section is about the frontend/entry point of the plugin that plays the role to handle some page related operations.

MkdocsNotePlugin

Bases: BasePlugin[MkdocsNoteConfig]

Mkdocs Note Plugin entry point.

Source code in src/mkdocs_note/plugin.py
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
class MkdocsNotePlugin(BasePlugin[MkdocsNoteConfig]):
	"""Mkdocs Note Plugin entry point."""

	notes_list: ClassVar[list[File]] = []

	@event_priority(100)
	def on_files(self, files: Files, config: MkDocsConfig) -> Files:
		"""Handle file processing."""
		self.notes_list.clear()
		invalid_files: list[File] = []

		self.notes_list, invalid_files = scanner.scan_notes(files, self.config)

		self.notes_list.sort(key=lambda f: f.note_date, reverse=True)

		for f in invalid_files:
			files.remove(f)

		return files

	def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
		"""Handle plugin configuration."""
		self.static_dir = os.path.join(os.path.dirname(__file__), "static")

		add_static_resouces(config)

		if self.config.preview_config.get("enabled", False):
			add_preview_static_resources(config)

		return config

	def on_pre_build(self, *, config: dict, **kwagrs) -> None:
		"""Handle pre-build."""
		if self.config.graph_config["enabled"]:
			self._graph = Graph(self.config.graph_config)
		if self.config.preview_config.get("enabled", False):
			from urllib.parse import urlparse

			site_url = config.get("site_url") if hasattr(config, "get") else None
			if site_url:
				base = urlparse(site_url).path
				if not base.endswith("/"):
					base += "/"
			else:
				base = "/"

			extra_src: set[str] = set()
			if self.config.recent_notes_config.get("enabled", False):
				n = int(self.config.recent_notes_config.get("insert_num", 10))
				extra_src = {f.src_path for f in self.notes_list[:n]}

			self._preview = PreviewBuilder(
				self.config.preview_config,
				site_base=base,
				markdown_extensions=config.get("markdown_extensions")
				if hasattr(config, "get")
				else None,
				mdx_configs=config.get("mdx_configs")
				if hasattr(config, "get")
				else None,
				extra_src_paths=extra_src,
			)

	def on_nav(
		self,
		nav: Navigation,
		*,
		config: MkDocsConfig,
		files: Files,
	) -> Navigation:
		"""Handle navigation.

		Args:
			nav (Navigation): The navigation object.
			config (MkDocsConfig): The MkDocs configuration.
			files (Files): The files object.

		Returns:
			Navigation: The navigation object.
		"""
		self._files = files
		return nav

	def _write_graph_file(self, config: MkDocsConfig) -> None:
		"""Write the graph data to a file."""
		log.info("Writing graph data to file...")
		output_dir = os.path.join(config["site_dir"], "graph")
		try:
			os.makedirs(output_dir, exist_ok=True)
			graph_file = os.path.join(output_dir, "graph.json")
			with open(graph_file, "w") as f:
				json.dump(self._graph.to_dict(), f)
		except OSError as e:
			log.error(f"Error writing graph file: {e}")

	def on_post_page(
		self,
		output: str,
		*,
		page: Page,
		config: MkDocsConfig,
	) -> str:
		"""Handle post page.

		Args:
			output (str): The output content.
			page (Page): The page object.
			config (MkDocsConfig): The MkDocs configuration.

		Returns:
			str: The output content.
		"""
		debug = self.config.graph_config.get("debug", False)
		output = inject_graph_script(output=output, config=config, debug=debug)

		if self.config.preview_config.get("enabled", False):
			output = inject_preview_script(
				output,
				config,
				self.config.preview_config,
				graph_enabled=bool(self.config.graph_config.get("enabled", False)),
			)
		return output

	def on_post_build(
		self,
		*,
		config: MkDocsConfig,
		**kwargs,
	) -> None:
		"""Handle post build.

		Args:
			config (MkDocsConfig): The MkDocs configuration.
		"""
		# Build graph if enabled
		if hasattr(self, "_graph") and hasattr(self, "_files"):
			self._graph(self._files)
			self._write_graph_file(config=config)

		if hasattr(self, "_preview") and hasattr(self, "_files"):
			try:
				# Refresh linked_only expansion sets at build time
				if self.config.recent_notes_config.get("enabled", False):
					n = int(self.config.recent_notes_config.get("insert_num", 10))
					self._preview.extra_src_paths = {
						f.src_path for f in self.notes_list[:n]
					}
				index_src: set[str] = set()
				for f in self._files.documentation_pages():
					if self.is_note_index_page(f):
						index_src.add(f.src_path)
				self._preview.index_src_paths = index_src
				if hasattr(self, "_graph") and getattr(self._graph, "nodes", None):
					self._preview.graph_src_paths = {
						n["id"] for n in self._graph.nodes if "id" in n
					}
				data = self._preview(self._files)
				write_previews_file(data, config)
			except OSError as e:
				log.error(f"Error writing previews file: {e}")

		log.info("Copying static assets...")
		try:
			copy_static_assets(static_dir=self.static_dir, config=config)
		except OSError as e:
			log.error(f"Error copying static assets: {e}")

		if self.config.preview_config.get("enabled", False):
			try:
				copy_preview_static_assets(static_dir=self.static_dir, config=config)
			except OSError as e:
				log.error(f"Error copying preview static assets: {e}")

	def on_page_markdown(
		self,
		markdown: str,
		page: Page,
		config: MkDocsConfig,
		files: Files,
	) -> str:
		"""Handle page markdown.

		Args:
			markdown (str): The markdown content.
			page (Page): The page object.
			config (MkDocsConfig): The MkDocs configuration.
			files (Files): The files object.

		Returns:
			str: The markdown content.
		"""
		# Only process recent notes on the note index page
		if self.config.recent_notes_config["enabled"] and self.is_note_index_page(
			page.file
		):
			markdown = insert_recent_note_links(
				markdown=markdown,
				notes_list=self.notes_list,
				insert_num=self.config.recent_notes_config["insert_num"],
				replace_marker=self.config.recent_notes_config["insert_marker"],
			)
			log.info(
				f"Inserted {self.config.recent_notes_config['insert_num']} recent notes into {page.file.src_uri}"
			)

		return markdown

	def is_note_index_page(self, f: File) -> bool:
		"""Check if the page is a note index page.

		Args:
			f (File): The file to check.

		Returns:
			bool: True if the page is a note index page, False otherwise.
		"""
		from pathlib import Path

		# Check if file ends with index.md
		if not f.src_uri.endswith("index.md"):
			return False

		# Check if file is the index.md directly under notes_root
		try:
			notes_dir = (
				Path(self.config.notes_root)
				if isinstance(self.config.notes_root, str)
				else self.config.notes_root
			)
			file_path = Path(f.abs_src_path)
			# Get the relative path from notes_root to the file
			rel_path = file_path.relative_to(notes_dir)
			# If the relative path is exactly "index.md", it's the index page
			return str(rel_path) == "index.md"
		except (ValueError, AttributeError):
			# File is not within notes_root
			return False

is_note_index_page(f)

Check if the page is a note index page.

Parameters:

Name Type Description Default
f File

The file to check.

required

Returns:

Name Type Description
bool bool

True if the page is a note index page, False otherwise.

Source code in src/mkdocs_note/plugin.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def is_note_index_page(self, f: File) -> bool:
	"""Check if the page is a note index page.

	Args:
		f (File): The file to check.

	Returns:
		bool: True if the page is a note index page, False otherwise.
	"""
	from pathlib import Path

	# Check if file ends with index.md
	if not f.src_uri.endswith("index.md"):
		return False

	# Check if file is the index.md directly under notes_root
	try:
		notes_dir = (
			Path(self.config.notes_root)
			if isinstance(self.config.notes_root, str)
			else self.config.notes_root
		)
		file_path = Path(f.abs_src_path)
		# Get the relative path from notes_root to the file
		rel_path = file_path.relative_to(notes_dir)
		# If the relative path is exactly "index.md", it's the index page
		return str(rel_path) == "index.md"
	except (ValueError, AttributeError):
		# File is not within notes_root
		return False

on_config(config)

Handle plugin configuration.

Source code in src/mkdocs_note/plugin.py
53
54
55
56
57
58
59
60
61
62
def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
	"""Handle plugin configuration."""
	self.static_dir = os.path.join(os.path.dirname(__file__), "static")

	add_static_resouces(config)

	if self.config.preview_config.get("enabled", False):
		add_preview_static_resources(config)

	return config

on_files(files, config)

Handle file processing.

Source code in src/mkdocs_note/plugin.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@event_priority(100)
def on_files(self, files: Files, config: MkDocsConfig) -> Files:
	"""Handle file processing."""
	self.notes_list.clear()
	invalid_files: list[File] = []

	self.notes_list, invalid_files = scanner.scan_notes(files, self.config)

	self.notes_list.sort(key=lambda f: f.note_date, reverse=True)

	for f in invalid_files:
		files.remove(f)

	return files

on_nav(nav, *, config, files)

Handle navigation.

Parameters:

Name Type Description Default
nav Navigation

The navigation object.

required
config MkDocsConfig

The MkDocs configuration.

required
files Files

The files object.

required

Returns:

Name Type Description
Navigation Navigation

The navigation object.

Source code in src/mkdocs_note/plugin.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def on_nav(
	self,
	nav: Navigation,
	*,
	config: MkDocsConfig,
	files: Files,
) -> Navigation:
	"""Handle navigation.

	Args:
		nav (Navigation): The navigation object.
		config (MkDocsConfig): The MkDocs configuration.
		files (Files): The files object.

	Returns:
		Navigation: The navigation object.
	"""
	self._files = files
	return nav

on_page_markdown(markdown, page, config, files)

Handle page markdown.

Parameters:

Name Type Description Default
markdown str

The markdown content.

required
page Page

The page object.

required
config MkDocsConfig

The MkDocs configuration.

required
files Files

The files object.

required

Returns:

Name Type Description
str str

The markdown content.

Source code in src/mkdocs_note/plugin.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def on_page_markdown(
	self,
	markdown: str,
	page: Page,
	config: MkDocsConfig,
	files: Files,
) -> str:
	"""Handle page markdown.

	Args:
		markdown (str): The markdown content.
		page (Page): The page object.
		config (MkDocsConfig): The MkDocs configuration.
		files (Files): The files object.

	Returns:
		str: The markdown content.
	"""
	# Only process recent notes on the note index page
	if self.config.recent_notes_config["enabled"] and self.is_note_index_page(
		page.file
	):
		markdown = insert_recent_note_links(
			markdown=markdown,
			notes_list=self.notes_list,
			insert_num=self.config.recent_notes_config["insert_num"],
			replace_marker=self.config.recent_notes_config["insert_marker"],
		)
		log.info(
			f"Inserted {self.config.recent_notes_config['insert_num']} recent notes into {page.file.src_uri}"
		)

	return markdown

on_post_build(*, config, **kwargs)

Handle post build.

Parameters:

Name Type Description Default
config MkDocsConfig

The MkDocs configuration.

required
Source code in src/mkdocs_note/plugin.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def on_post_build(
	self,
	*,
	config: MkDocsConfig,
	**kwargs,
) -> None:
	"""Handle post build.

	Args:
		config (MkDocsConfig): The MkDocs configuration.
	"""
	# Build graph if enabled
	if hasattr(self, "_graph") and hasattr(self, "_files"):
		self._graph(self._files)
		self._write_graph_file(config=config)

	if hasattr(self, "_preview") and hasattr(self, "_files"):
		try:
			# Refresh linked_only expansion sets at build time
			if self.config.recent_notes_config.get("enabled", False):
				n = int(self.config.recent_notes_config.get("insert_num", 10))
				self._preview.extra_src_paths = {
					f.src_path for f in self.notes_list[:n]
				}
			index_src: set[str] = set()
			for f in self._files.documentation_pages():
				if self.is_note_index_page(f):
					index_src.add(f.src_path)
			self._preview.index_src_paths = index_src
			if hasattr(self, "_graph") and getattr(self._graph, "nodes", None):
				self._preview.graph_src_paths = {
					n["id"] for n in self._graph.nodes if "id" in n
				}
			data = self._preview(self._files)
			write_previews_file(data, config)
		except OSError as e:
			log.error(f"Error writing previews file: {e}")

	log.info("Copying static assets...")
	try:
		copy_static_assets(static_dir=self.static_dir, config=config)
	except OSError as e:
		log.error(f"Error copying static assets: {e}")

	if self.config.preview_config.get("enabled", False):
		try:
			copy_preview_static_assets(static_dir=self.static_dir, config=config)
		except OSError as e:
			log.error(f"Error copying preview static assets: {e}")

on_post_page(output, *, page, config)

Handle post page.

Parameters:

Name Type Description Default
output str

The output content.

required
page Page

The page object.

required
config MkDocsConfig

The MkDocs configuration.

required

Returns:

Name Type Description
str str

The output content.

Source code in src/mkdocs_note/plugin.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def on_post_page(
	self,
	output: str,
	*,
	page: Page,
	config: MkDocsConfig,
) -> str:
	"""Handle post page.

	Args:
		output (str): The output content.
		page (Page): The page object.
		config (MkDocsConfig): The MkDocs configuration.

	Returns:
		str: The output content.
	"""
	debug = self.config.graph_config.get("debug", False)
	output = inject_graph_script(output=output, config=config, debug=debug)

	if self.config.preview_config.get("enabled", False):
		output = inject_preview_script(
			output,
			config,
			self.config.preview_config,
			graph_enabled=bool(self.config.graph_config.get("enabled", False)),
		)
	return output

on_pre_build(*, config, **kwagrs)

Handle pre-build.

Source code in src/mkdocs_note/plugin.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def on_pre_build(self, *, config: dict, **kwagrs) -> None:
	"""Handle pre-build."""
	if self.config.graph_config["enabled"]:
		self._graph = Graph(self.config.graph_config)
	if self.config.preview_config.get("enabled", False):
		from urllib.parse import urlparse

		site_url = config.get("site_url") if hasattr(config, "get") else None
		if site_url:
			base = urlparse(site_url).path
			if not base.endswith("/"):
				base += "/"
		else:
			base = "/"

		extra_src: set[str] = set()
		if self.config.recent_notes_config.get("enabled", False):
			n = int(self.config.recent_notes_config.get("insert_num", 10))
			extra_src = {f.src_path for f in self.notes_list[:n]}

		self._preview = PreviewBuilder(
			self.config.preview_config,
			site_base=base,
			markdown_extensions=config.get("markdown_extensions")
			if hasattr(config, "get")
			else None,
			mdx_configs=config.get("mdx_configs")
			if hasattr(config, "get")
			else None,
			extra_src_paths=extra_src,
		)

Insert recent note links into the markdown.

Parameters:

Name Type Description Default
markdown str

The markdown content.

required
notes_list list[File]

The list of valid notes.

required
insert_num int

The number of recent notes to insert.

required
replace_marker str

The marker to replace.

required

Returns:

Name Type Description
str str

The markdown content with recent note links inserted.

Source code in src/mkdocs_note/plugin.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def insert_recent_note_links(
	markdown: str,
	notes_list: list[File],
	insert_num: int,
	replace_marker: str,
) -> str:
	"""Insert recent note links into the markdown.

	Args:
	    markdown (str): The markdown content.
	    notes_list (list[File]): The list of valid notes.
	    insert_num (int): The number of recent notes to insert.
	    replace_marker (str): The marker to replace.

	Returns:
	    str: The markdown content with recent note links inserted.
	"""

	content = "<ul>\n"
	for f in notes_list[:insert_num]:
		title = extract_title(f)
		date = extract_date(f).strftime("%Y-%m-%d %H:%M:%S")
		content += f'<li><div style="display:flex; justify-content:space-between; align-items:center;"><a href="{f.page.abs_url}">{title}</a><span style="font-size:0.8em; color:#888;">{date}</span></div></li>\n'
	content += "</ul>\n"
	return markdown.replace(replace_marker, content)