We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
feat: Add `page_number` to meta of Documents in `DocumentSplitter`
**Is your feature request related to a problem? Please describe.**
In Haystack v1 we had an option in the Preprocessor to add the original `page_number` to a Document's meta data when it was split into a chunk. This feature made down stream applications of visualizing the retrieved text from original files (e.g. PDFs) very easy and straightforward, so I'd like to see it in Haystack v2 as well. 

**Describe the solution you'd like**
I would like to add the option to store the `page_number` in the meta info of the Document in the DocumentSplitter component. I believe we can use a similar/same implementation of calculating this like we did for the Preprocessor. 


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `haystack/components/preprocessors/document_splitter.py`
Content:
```
1 from copy import deepcopy
2 from typing import List, Literal
3 
4 from more_itertools import windowed
5 
6 from haystack import Document, component
7 
8 
9 @component
10 class DocumentSplitter:
11     """
12     Splits a list of text documents into a list of text documents with shorter texts.
13 
14     Splitting documents with long texts is a common preprocessing step during indexing.
15     This allows Embedders to create significant semantic representations
16     and avoids exceeding the maximum context length of language models.
17     """
18 
19     def __init__(
20         self,
21         split_by: Literal["word", "sentence", "page", "passage"] = "word",
22         split_length: int = 200,
23         split_overlap: int = 0,
24     ):
25         """
26         Initialize the DocumentSplitter.
27 
28         :param split_by: The unit by which the document should be split. Choose from "word" for splitting by " ",
29             "sentence" for splitting by ".", "page" for splitting by "\\f" or "passage" for splitting by "\\n\\n".
30         :param split_length: The maximum number of units in each split.
31         :param split_overlap: The number of units that each split should overlap.
32         """
33 
34         self.split_by = split_by
35         if split_by not in ["word", "sentence", "page", "passage"]:
36             raise ValueError("split_by must be one of 'word', 'sentence', 'page' or 'passage'.")
37         if split_length <= 0:
38             raise ValueError("split_length must be greater than 0.")
39         self.split_length = split_length
40         if split_overlap < 0:
41             raise ValueError("split_overlap must be greater than or equal to 0.")
42         self.split_overlap = split_overlap
43 
44     @component.output_types(documents=List[Document])
45     def run(self, documents: List[Document]):
46         """
47         Split documents into smaller parts.
48 
49         Splits documents by the unit expressed in `split_by`, with a length of `split_length`
50         and an overlap of `split_overlap`.
51 
52         :param documents: The documents to split.
53 
54         :returns: A dictionary with the following key:
55             - `documents`: List of documents with the split texts. A metadata field "source_id" is added to each
56             document to keep track of the original document that was split. Other metadata are copied from the original
57             document.
58 
59         :raises TypeError: if the input is not a list of Documents.
60         :raises ValueError: if the content of a document is None.
61         """
62 
63         if not isinstance(documents, list) or (documents and not isinstance(documents[0], Document)):
64             raise TypeError("DocumentSplitter expects a List of Documents as input.")
65 
66         split_docs = []
67         for doc in documents:
68             if doc.content is None:
69                 raise ValueError(
70                     f"DocumentSplitter only works with text documents but document.content for document ID {doc.id} is None."
71                 )
72             units = self._split_into_units(doc.content, self.split_by)
73             text_splits = self._concatenate_units(units, self.split_length, self.split_overlap)
74             metadata = deepcopy(doc.meta)
75             metadata["source_id"] = doc.id
76             split_docs += [Document(content=txt, meta=metadata) for txt in text_splits]
77         return {"documents": split_docs}
78 
79     def _split_into_units(self, text: str, split_by: Literal["word", "sentence", "passage", "page"]) -> List[str]:
80         if split_by == "page":
81             split_at = "\f"
82         elif split_by == "passage":
83             split_at = "\n\n"
84         elif split_by == "sentence":
85             split_at = "."
86         elif split_by == "word":
87             split_at = " "
88         else:
89             raise NotImplementedError(
90                 "DocumentSplitter only supports 'word', 'sentence', 'page' or 'passage' split_by options."
91             )
92         units = text.split(split_at)
93         # Add the delimiter back to all units except the last one
94         for i in range(len(units) - 1):
95             units[i] += split_at
96         return units
97 
98     def _concatenate_units(self, elements: List[str], split_length: int, split_overlap: int) -> List[str]:
99         """
100         Concatenates the elements into parts of split_length units.
101         """
102         text_splits = []
103         segments = windowed(elements, n=split_length, step=split_length - split_overlap)
104         for seg in segments:
105             current_units = [unit for unit in seg if unit is not None]
106             txt = "".join(current_units)
107             if len(txt) > 0:
108                 text_splits.append(txt)
109         return text_splits
110
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/haystack/components/preprocessors/document_splitter.py b/haystack/components/preprocessors/document_splitter.py
--- a/haystack/components/preprocessors/document_splitter.py
+++ b/haystack/components/preprocessors/document_splitter.py
@@ -1,5 +1,5 @@
 from copy import deepcopy
-from typing import List, Literal
+from typing import Dict, List, Literal, Tuple
 
 from more_itertools import windowed
 
@@ -53,7 +53,7 @@
 
         :returns: A dictionary with the following key:
             - `documents`: List of documents with the split texts. A metadata field "source_id" is added to each
-            document to keep track of the original document that was split. Other metadata are copied from the original
+            document to keep track of the original document that was split. Another metadata field "page_number" is added to each number to keep track of the page it belonged to in the original document. Other metadata are copied from the original
             document.
 
         :raises TypeError: if the input is not a list of Documents.
@@ -70,10 +70,12 @@
                     f"DocumentSplitter only works with text documents but document.content for document ID {doc.id} is None."
                 )
             units = self._split_into_units(doc.content, self.split_by)
-            text_splits = self._concatenate_units(units, self.split_length, self.split_overlap)
+            text_splits, splits_pages = self._concatenate_units(units, self.split_length, self.split_overlap)
             metadata = deepcopy(doc.meta)
             metadata["source_id"] = doc.id
-            split_docs += [Document(content=txt, meta=metadata) for txt in text_splits]
+            split_docs += self._create_docs_from_splits(
+                text_splits=text_splits, splits_pages=splits_pages, meta=metadata
+            )
         return {"documents": split_docs}
 
     def _split_into_units(self, text: str, split_by: Literal["word", "sentence", "passage", "page"]) -> List[str]:
@@ -95,15 +97,40 @@
             units[i] += split_at
         return units
 
-    def _concatenate_units(self, elements: List[str], split_length: int, split_overlap: int) -> List[str]:
+    def _concatenate_units(
+        self, elements: List[str], split_length: int, split_overlap: int
+    ) -> Tuple[List[str], List[int]]:
         """
-        Concatenates the elements into parts of split_length units.
+        Concatenates the elements into parts of split_length units keeping track of the original page number that each element belongs.
         """
         text_splits = []
+        splits_pages = []
+        cur_page = 1
         segments = windowed(elements, n=split_length, step=split_length - split_overlap)
         for seg in segments:
             current_units = [unit for unit in seg if unit is not None]
             txt = "".join(current_units)
             if len(txt) > 0:
                 text_splits.append(txt)
-        return text_splits
+                splits_pages.append(cur_page)
+                processed_units = current_units[: split_length - split_overlap]
+                if self.split_by == "page":
+                    num_page_breaks = len(processed_units)
+                else:
+                    num_page_breaks = sum(processed_unit.count("\f") for processed_unit in processed_units)
+                cur_page += num_page_breaks
+        return text_splits, splits_pages
+
+    @staticmethod
+    def _create_docs_from_splits(text_splits: List[str], splits_pages: List[int], meta: Dict) -> List[Document]:
+        """
+        Creates Document objects from text splits enriching them with page number and the metadata of the original document.
+        """
+        documents: List[Document] = []
+
+        for i, txt in enumerate(text_splits):
+            meta = deepcopy(meta)
+            doc = Document(content=txt, meta=meta)
+            doc.meta["page_number"] = splits_pages[i]
+            documents.append(doc)
+        return documents

{"golden_diff": "diff --git a/haystack/components/preprocessors/document_splitter.py b/haystack/components/preprocessors/document_splitter.py\n--- a/haystack/components/preprocessors/document_splitter.py\n+++ b/haystack/components/preprocessors/document_splitter.py\n@@ -1,5 +1,5 @@\n from copy import deepcopy\n-from typing import List, Literal\n+from typing import Dict, List, Literal, Tuple\n \n from more_itertools import windowed\n \n@@ -53,7 +53,7 @@\n \n         :returns: A dictionary with the following key:\n             - `documents`: List of documents with the split texts. A metadata field \"source_id\" is added to each\n-            document to keep track of the original document that was split. Other metadata are copied from the original\n+            document to keep track of the original document that was split. Another metadata field \"page_number\" is added to each number to keep track of the page it belonged to in the original document. Other metadata are copied from the original\n             document.\n \n         :raises TypeError: if the input is not a list of Documents.\n@@ -70,10 +70,12 @@\n                     f\"DocumentSplitter only works with text documents but document.content for document ID {doc.id} is None.\"\n                 )\n             units = self._split_into_units(doc.content, self.split_by)\n-            text_splits = self._concatenate_units(units, self.split_length, self.split_overlap)\n+            text_splits, splits_pages = self._concatenate_units(units, self.split_length, self.split_overlap)\n             metadata = deepcopy(doc.meta)\n             metadata[\"source_id\"] = doc.id\n-            split_docs += [Document(content=txt, meta=metadata) for txt in text_splits]\n+            split_docs += self._create_docs_from_splits(\n+                text_splits=text_splits, splits_pages=splits_pages, meta=metadata\n+            )\n         return {\"documents\": split_docs}\n \n     def _split_into_units(self, text: str, split_by: Literal[\"word\", \"sentence\", \"passage\", \"page\"]) -> List[str]:\n@@ -95,15 +97,40 @@\n             units[i] += split_at\n         return units\n \n-    def _concatenate_units(self, elements: List[str], split_length: int, split_overlap: int) -> List[str]:\n+    def _concatenate_units(\n+        self, elements: List[str], split_length: int, split_overlap: int\n+    ) -> Tuple[List[str], List[int]]:\n         \"\"\"\n-        Concatenates the elements into parts of split_length units.\n+        Concatenates the elements into parts of split_length units keeping track of the original page number that each element belongs.\n         \"\"\"\n         text_splits = []\n+        splits_pages = []\n+        cur_page = 1\n         segments = windowed(elements, n=split_length, step=split_length - split_overlap)\n         for seg in segments:\n             current_units = [unit for unit in seg if unit is not None]\n             txt = \"\".join(current_units)\n             if len(txt) > 0:\n                 text_splits.append(txt)\n-        return text_splits\n+                splits_pages.append(cur_page)\n+                processed_units = current_units[: split_length - split_overlap]\n+                if self.split_by == \"page\":\n+                    num_page_breaks = len(processed_units)\n+                else:\n+                    num_page_breaks = sum(processed_unit.count(\"\\f\") for processed_unit in processed_units)\n+                cur_page += num_page_breaks\n+        return text_splits, splits_pages\n+\n+    @staticmethod\n+    def _create_docs_from_splits(text_splits: List[str], splits_pages: List[int], meta: Dict) -> List[Document]:\n+        \"\"\"\n+        Creates Document objects from text splits enriching them with page number and the metadata of the original document.\n+        \"\"\"\n+        documents: List[Document] = []\n+\n+        for i, txt in enumerate(text_splits):\n+            meta = deepcopy(meta)\n+            doc = Document(content=txt, meta=meta)\n+            doc.meta[\"page_number\"] = splits_pages[i]\n+            documents.append(doc)\n+        return documents\n", "issue": "feat: Add `page_number` to meta of Documents in `DocumentSplitter`\n**Is your feature request related to a problem? Please describe.**\r\nIn Haystack v1 we had an option in the Preprocessor to add the original `page_number` to a Document's meta data when it was split into a chunk. This feature made down stream applications of visualizing the retrieved text from original files (e.g. PDFs) very easy and straightforward, so I'd like to see it in Haystack v2 as well. \r\n\r\n**Describe the solution you'd like**\r\nI would like to add the option to store the `page_number` in the meta info of the Document in the DocumentSplitter component. I believe we can use a similar/same implementation of calculating this like we did for the Preprocessor. \r\n\n", "before_files": [{"content": "from copy import deepcopy\nfrom typing import List, Literal\n\nfrom more_itertools import windowed\n\nfrom haystack import Document, component\n\n\n@component\nclass DocumentSplitter:\n    \"\"\"\n    Splits a list of text documents into a list of text documents with shorter texts.\n\n    Splitting documents with long texts is a common preprocessing step during indexing.\n    This allows Embedders to create significant semantic representations\n    and avoids exceeding the maximum context length of language models.\n    \"\"\"\n\n    def __init__(\n        self,\n        split_by: Literal[\"word\", \"sentence\", \"page\", \"passage\"] = \"word\",\n        split_length: int = 200,\n        split_overlap: int = 0,\n    ):\n        \"\"\"\n        Initialize the DocumentSplitter.\n\n        :param split_by: The unit by which the document should be split. Choose from \"word\" for splitting by \" \",\n            \"sentence\" for splitting by \".\", \"page\" for splitting by \"\\\\f\" or \"passage\" for splitting by \"\\\\n\\\\n\".\n        :param split_length: The maximum number of units in each split.\n        :param split_overlap: The number of units that each split should overlap.\n        \"\"\"\n\n        self.split_by = split_by\n        if split_by not in [\"word\", \"sentence\", \"page\", \"passage\"]:\n            raise ValueError(\"split_by must be one of 'word', 'sentence', 'page' or 'passage'.\")\n        if split_length <= 0:\n            raise ValueError(\"split_length must be greater than 0.\")\n        self.split_length = split_length\n        if split_overlap < 0:\n            raise ValueError(\"split_overlap must be greater than or equal to 0.\")\n        self.split_overlap = split_overlap\n\n    @component.output_types(documents=List[Document])\n    def run(self, documents: List[Document]):\n        \"\"\"\n        Split documents into smaller parts.\n\n        Splits documents by the unit expressed in `split_by`, with a length of `split_length`\n        and an overlap of `split_overlap`.\n\n        :param documents: The documents to split.\n\n        :returns: A dictionary with the following key:\n            - `documents`: List of documents with the split texts. A metadata field \"source_id\" is added to each\n            document to keep track of the original document that was split. Other metadata are copied from the original\n            document.\n\n        :raises TypeError: if the input is not a list of Documents.\n        :raises ValueError: if the content of a document is None.\n        \"\"\"\n\n        if not isinstance(documents, list) or (documents and not isinstance(documents[0], Document)):\n            raise TypeError(\"DocumentSplitter expects a List of Documents as input.\")\n\n        split_docs = []\n        for doc in documents:\n            if doc.content is None:\n                raise ValueError(\n                    f\"DocumentSplitter only works with text documents but document.content for document ID {doc.id} is None.\"\n                )\n            units = self._split_into_units(doc.content, self.split_by)\n            text_splits = self._concatenate_units(units, self.split_length, self.split_overlap)\n            metadata = deepcopy(doc.meta)\n            metadata[\"source_id\"] = doc.id\n            split_docs += [Document(content=txt, meta=metadata) for txt in text_splits]\n        return {\"documents\": split_docs}\n\n    def _split_into_units(self, text: str, split_by: Literal[\"word\", \"sentence\", \"passage\", \"page\"]) -> List[str]:\n        if split_by == \"page\":\n            split_at = \"\\f\"\n        elif split_by == \"passage\":\n            split_at = \"\\n\\n\"\n        elif split_by == \"sentence\":\n            split_at = \".\"\n        elif split_by == \"word\":\n            split_at = \" \"\n        else:\n            raise NotImplementedError(\n                \"DocumentSplitter only supports 'word', 'sentence', 'page' or 'passage' split_by options.\"\n            )\n        units = text.split(split_at)\n        # Add the delimiter back to all units except the last one\n        for i in range(len(units) - 1):\n            units[i] += split_at\n        return units\n\n    def _concatenate_units(self, elements: List[str], split_length: int, split_overlap: int) -> List[str]:\n        \"\"\"\n        Concatenates the elements into parts of split_length units.\n        \"\"\"\n        text_splits = []\n        segments = windowed(elements, n=split_length, step=split_length - split_overlap)\n        for seg in segments:\n            current_units = [unit for unit in seg if unit is not None]\n            txt = \"\".join(current_units)\n            if len(txt) > 0:\n                text_splits.append(txt)\n        return text_splits\n", "path": "haystack/components/preprocessors/document_splitter.py"}], "after_files": [{"content": "from copy import deepcopy\nfrom typing import Dict, List, Literal, Tuple\n\nfrom more_itertools import windowed\n\nfrom haystack import Document, component\n\n\n@component\nclass DocumentSplitter:\n    \"\"\"\n    Splits a list of text documents into a list of text documents with shorter texts.\n\n    Splitting documents with long texts is a common preprocessing step during indexing.\n    This allows Embedders to create significant semantic representations\n    and avoids exceeding the maximum context length of language models.\n    \"\"\"\n\n    def __init__(\n        self,\n        split_by: Literal[\"word\", \"sentence\", \"page\", \"passage\"] = \"word\",\n        split_length: int = 200,\n        split_overlap: int = 0,\n    ):\n        \"\"\"\n        Initialize the DocumentSplitter.\n\n        :param split_by: The unit by which the document should be split. Choose from \"word\" for splitting by \" \",\n            \"sentence\" for splitting by \".\", \"page\" for splitting by \"\\\\f\" or \"passage\" for splitting by \"\\\\n\\\\n\".\n        :param split_length: The maximum number of units in each split.\n        :param split_overlap: The number of units that each split should overlap.\n        \"\"\"\n\n        self.split_by = split_by\n        if split_by not in [\"word\", \"sentence\", \"page\", \"passage\"]:\n            raise ValueError(\"split_by must be one of 'word', 'sentence', 'page' or 'passage'.\")\n        if split_length <= 0:\n            raise ValueError(\"split_length must be greater than 0.\")\n        self.split_length = split_length\n        if split_overlap < 0:\n            raise ValueError(\"split_overlap must be greater than or equal to 0.\")\n        self.split_overlap = split_overlap\n\n    @component.output_types(documents=List[Document])\n    def run(self, documents: List[Document]):\n        \"\"\"\n        Split documents into smaller parts.\n\n        Splits documents by the unit expressed in `split_by`, with a length of `split_length`\n        and an overlap of `split_overlap`.\n\n        :param documents: The documents to split.\n\n        :returns: A dictionary with the following key:\n            - `documents`: List of documents with the split texts. A metadata field \"source_id\" is added to each\n            document to keep track of the original document that was split. Another metadata field \"page_number\" is added to each number to keep track of the page it belonged to in the original document. Other metadata are copied from the original\n            document.\n\n        :raises TypeError: if the input is not a list of Documents.\n        :raises ValueError: if the content of a document is None.\n        \"\"\"\n\n        if not isinstance(documents, list) or (documents and not isinstance(documents[0], Document)):\n            raise TypeError(\"DocumentSplitter expects a List of Documents as input.\")\n\n        split_docs = []\n        for doc in documents:\n            if doc.content is None:\n                raise ValueError(\n                    f\"DocumentSplitter only works with text documents but document.content for document ID {doc.id} is None.\"\n                )\n            units = self._split_into_units(doc.content, self.split_by)\n            text_splits, splits_pages = self._concatenate_units(units, self.split_length, self.split_overlap)\n            metadata = deepcopy(doc.meta)\n            metadata[\"source_id\"] = doc.id\n            split_docs += self._create_docs_from_splits(\n                text_splits=text_splits, splits_pages=splits_pages, meta=metadata\n            )\n        return {\"documents\": split_docs}\n\n    def _split_into_units(self, text: str, split_by: Literal[\"word\", \"sentence\", \"passage\", \"page\"]) -> List[str]:\n        if split_by == \"page\":\n            split_at = \"\\f\"\n        elif split_by == \"passage\":\n            split_at = \"\\n\\n\"\n        elif split_by == \"sentence\":\n            split_at = \".\"\n        elif split_by == \"word\":\n            split_at = \" \"\n        else:\n            raise NotImplementedError(\n                \"DocumentSplitter only supports 'word', 'sentence', 'page' or 'passage' split_by options.\"\n            )\n        units = text.split(split_at)\n        # Add the delimiter back to all units except the last one\n        for i in range(len(units) - 1):\n            units[i] += split_at\n        return units\n\n    def _concatenate_units(\n        self, elements: List[str], split_length: int, split_overlap: int\n    ) -> Tuple[List[str], List[int]]:\n        \"\"\"\n        Concatenates the elements into parts of split_length units keeping track of the original page number that each element belongs.\n        \"\"\"\n        text_splits = []\n        splits_pages = []\n        cur_page = 1\n        segments = windowed(elements, n=split_length, step=split_length - split_overlap)\n        for seg in segments:\n            current_units = [unit for unit in seg if unit is not None]\n            txt = \"\".join(current_units)\n            if len(txt) > 0:\n                text_splits.append(txt)\n                splits_pages.append(cur_page)\n                processed_units = current_units[: split_length - split_overlap]\n                if self.split_by == \"page\":\n                    num_page_breaks = len(processed_units)\n                else:\n                    num_page_breaks = sum(processed_unit.count(\"\\f\") for processed_unit in processed_units)\n                cur_page += num_page_breaks\n        return text_splits, splits_pages\n\n    @staticmethod\n    def _create_docs_from_splits(text_splits: List[str], splits_pages: List[int], meta: Dict) -> List[Document]:\n        \"\"\"\n        Creates Document objects from text splits enriching them with page number and the metadata of the original document.\n        \"\"\"\n        documents: List[Document] = []\n\n        for i, txt in enumerate(text_splits):\n            meta = deepcopy(meta)\n            doc = Document(content=txt, meta=meta)\n            doc.meta[\"page_number\"] = splits_pages[i]\n            documents.append(doc)\n        return documents\n", "path": "haystack/components/preprocessors/document_splitter.py"}]}