Repository: sunpy/sunpy

Issue:
Make Map preserve the formatting of the HISTORY metadata
### Describe the feature

After reading in a fits file using Map, it seems that all the \n's are thrown away for the information in the HISTORY. That makes reading that metadata much harder. It'd be nice if SunPy could treat the HISTORY metadata the way astropy.io.fits does, which preserves its format. As an example, here's part of a XRT image's metadata as viewed with fitsheader:
```
HISTORY LEVEL1_PREP v2015.May.19 NOTE: No spot correction done for this image;  
HISTORY (cont'd) XRT_SPOTCOR information applies to associated jpeg files only  
HISTORY XRT_SPOTCOR v2014-Apr-14: (23-May-2015 00:52:36) All spots and dust     
HISTORY (cont'd) corrected with thin plate splines. Spot mastermap version =    
HISTORY (cont'd) v20080206. Threshold = 0.500000
```

Here's what that part looks like after being read in by Map and then printed via print(m.fits_header)
```
HISTORY gin(cont'd) file: XRT20120605_215839.9.fitsLEVEL1_PREP v2015.May.19 NOTE
HISTORY : No spot correction done for this image;(cont'd) XRT_SPOTCOR informatio
HISTORY n applies to associated jpeg files onlyXRT_SPOTCOR v2014-Apr-14: (23-May
HISTORY -2015 00:52:36) All spots and dust(cont'd) corrected with thin plate spl
HISTORY ines. Spot mastermap version =(cont'd) v20080206. Threshold = 0.500000XR
```
### Proposed solution

I'm not sure where or how SunPy joins the lines in the HISTORY metadata, but it seems that the line breaks are somehow removed at that point. So, it may be as easy as not removing the line breaks. I looked through the code a bit and didn't see any place where HISTORY metadata is treated differently than any other data in the map code. In astropy there is a separate type of header keyword called a commentary_keyword. Maybe SunPy needs something like that.


Discussion:
The relevant code here is actually with the way `sunpy.io` parses the HISTORY key,

https://github.com/sunpy/sunpy/blob/61047b4b8a1d47e29d3417e419197931b89e1b4a/sunpy/io/_fits.py#L157

When parsing the metadata to pass it to `Map`, each key must be unique because the metadata is exposed a dictionary which is why all of the `HISTORY` keys are joined. That being said, I'm not sure there's any reason we couldn't just join each `HISTORY` entry with a newline character so that it would at least print more nicely to the screen. 

This is also relevant to #931 (opened almost exactly 9 years ago!) and probably something we should consider as we are rethinking the sunpy metadata model c.f. #4194 
So a simple fix to the formatting problem is to just remove the .strip() from that line.
I wonder though why SunPy needs define its own metadata format. astropy is already a requirement. Why not just use their Header class to store the metadata?
> So a simple fix to the formatting problem is to just remove the .strip() from that line.

The problem is `"".join()`. All the history cards are being joined together with no characters in between. Joining all the characters with newlines in between should fix this issue, e.g. `"\n".join()`. The `strip()` call is just stripping the leading and trailing white space. 

> I wonder though why SunPy needs define its own metadata format. astropy is already a requirement. Why not just use their Header class to store the metadata?

This is a fair question. One advantage of `MetaDict` is that it is case-insensitive (unlike `astropy.io.fits.Header`).  Additionally, the `MetaDict` approach is a quite old part of `sunpy`. See #49. It is possible that `astropy.io.fits.Header` was not nearly as mature when `MetaDict`/`MapHeader` was developed. Also, as outlined in that very old PR, by not tying the metadata object directly to the FITS header, we have the advantage of a metadata model that is independent of file format/the FITS standard. Of course, in practice, Maps have been used almost exclusively with FITS data (with the exception of JPEG-2000 images).

Moving forward, this independence from FITS will only become more important as non-FITS WCS models (e.g. gwcs) become more prevalent so I don't think it makes sense currently to replace our metadata model with `astropy.io.fits.Header`. We do not yet have a clear path forward on how to proceed when it comes to a new metadata model, but the a draft of the current thinking is outlined in sunpy/sunpy-SEP#61. Input is very much welcome!


Resolving patch:
diff --git a/sunpy/io/_fits.py b/sunpy/io/_fits.py
--- a/sunpy/io/_fits.py
+++ b/sunpy/io/_fits.py
@@ -150,11 +150,11 @@ def format_comments_and_history(input_header):
     `sunpy.io.header.FileHeader`
     """
     try:
-        comment = "".join(input_header['COMMENT']).strip()
+        comment = "\n".join(input_header['COMMENT']).strip()
     except KeyError:
         comment = ""
     try:
-        history = "".join(input_header['HISTORY']).strip()
+        history = "\n".join(input_header['HISTORY']).strip()
     except KeyError:
         history = ""
 
@@ -239,8 +239,17 @@ def header_to_fits(header):
     fits_header = fits.Header()
     # Check Header
     key_comments = header.pop('KEYCOMMENTS', False)
-
+    # This first iteration separates out COMMENTS and HISTORY into their own
+    # entries.
+    header_items = []
     for k, v in header.items():
+        if k.upper() in ('COMMENT', 'HV_COMMENT', 'HISTORY'):
+            for v_line in str(v).split('\n'):
+                header_items.append((k, v_line))
+        else:
+            header_items.append((k, v))
+
+    for k, v in header_items:
         # Drop any keys that have non-ascii characters
         if not fits.Card._ascii_text_re.match(str(v)):
             warn_metadata(f'The meta key {k} is not valid ascii, dropping from the FITS header')
@@ -257,13 +266,9 @@ def header_to_fits(header):
             continue
 
         if k.upper() in ('COMMENT', 'HV_COMMENT'):
-            comments = str(v).split('\n')
-            for com in comments:
-                fits_header.add_comment(com)
+            fits_header.add_comment(v)
         elif k.upper() == 'HISTORY':
-            hists = str(v).split('\n')
-            for hist in hists:
-                fits_header.add_history(hist)
+            fits_header.add_history(v)
         elif isinstance(v, fits.header._HeaderCommentaryCards):
             if k != '':
                 fits_header.append(fits.Card(k, str(v).split('\n')))


Test patch:
diff --git a/sunpy/io/tests/test_fits.py b/sunpy/io/tests/test_fits.py
--- a/sunpy/io/tests/test_fits.py
+++ b/sunpy/io/tests/test_fits.py
@@ -125,8 +125,7 @@ def test_write_with_metadict_header_astropy(tmpdir):
         data, header = fits_file[0].data, fits_file[0].header
     meta_header = MetaDict(OrderedDict(header))
     temp_file = tmpdir / "temp.fits"
-    with pytest.warns(SunpyMetadataWarning, match='The meta key comment is not valid ascii'):
-        _fits.write(str(temp_file), data, meta_header)
+    _fits.write(str(temp_file), data, meta_header)
     assert temp_file.exists()
     fits_file.close()
 
@@ -182,3 +181,48 @@ def test_read_memmap():
 
     data, _ = _fits.read(TEST_AIA_IMAGE, memmap=False)[0]
     assert data.base is None
+
+
+def test_merge_multiple_comment_history_entries():
+    header = fits.Header()
+    history = "First history entry\nSecond history entry"
+    comment = "First comment\nSecond comment"
+    for hist in history.split("\n"):
+        header.add_history(hist)
+    for com in comment.split("\n"):
+        header.add_comment(com)
+    file_header = _fits.format_comments_and_history(header)
+    assert file_header["HISTORY"] == history
+    assert file_header["COMMENT"] == comment
+
+
+def test_split_multiline_comment_history_entries():
+    header_dict = {
+        "history": "First history entry\nSecond history entry",
+        "comment": "First comment\nSecond comment",
+    }
+    fits_header = _fits.header_to_fits(header_dict)
+    for k, v in header_dict.items():
+        for v_fits, v_dict in zip(fits_header[k.upper()], v.split("\n")):
+            assert v_fits == v_dict
+
+
+def test_roundtrip_comment_history_entries(tmpdir):
+    # Construct FITS file from header with multiple history and comment cards
+    header = fits.Header.fromtextfile(get_test_filepath("solo_L1_eui-fsi304-image_20201021T145510206_V03.header"))
+    data = np.random.rand(header['NAXIS2'], header['NAXIS1'])
+    outfilename = str(tmpdir / 'roundtrip-comments-history-astropy.fits')
+    fits.writeto(outfilename, data, header)
+    # Check roundtripping
+    (dh_pair, ) = _fits.read(outfilename)
+    header_dict = dh_pair.header
+    assert header_dict["HISTORY"] == "\n".join(header["HISTORY"])
+    assert header_dict["COMMENT"] == "\n".join(header["COMMENT"])
+    outfilename = str(tmpdir / 'roundtrip-comments-history-sunpy.fits')
+    _fits.write(outfilename, data, header_dict)
+    with fits.open(outfilename) as hdul:
+        header_rt = hdul[0].header
+    for key in ("HISTORY", "COMMENT"):
+        assert len(header_rt[key]) == len(header[key])
+        for card_rt, card in zip(header_rt[key], header[key]):
+            assert card_rt == card