Google Spreadsheet のセルを装飾を含めて HTML にする

# Google スプレッドシートを『装飾も含めて』 HTML で見たい？

Google スプレッドシートを HTML で見るという記事を前に書いた。

https://qiita.com/Chrysanthemum94/items/7bdaee4b1e0b7642dca2

これをもう一歩進めて見た目も反映させたいという話がお仕事中に持ち上がったので書いたため、共有しておく。

つまり、この見た目をそのまま HTML として出力したい、という話である。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633175/b67948be-4a78-bbec-cf29-8e741cf86d78.png)

# 書き方

## 前提知識

### Range#getRichTextValue で装飾情報を取得

Google Apps Script の Range クラスには [getRichTextValue](https://developers.google.com/apps-script/reference/spreadsheet/range#getrichtextvalue) なる関数がある。

これの返り値は[RichTextValue クラス](https://developers.google.com/apps-script/reference/spreadsheet/rich-text-value)であり、その名の通りテキストスタイルを含むセルの情報が格納されるものである。よって、この情報を HTML にすればセルの内容を HTML にすることができる。

### getRuns() でセルの内容を分割した値を取得

RichTextValue が取得できたので早速装飾情報を取りたいが、装飾情報は入れ子になっていたりして複雑である。セルを文字列が装飾されている単位ごとに分割するには [getRuns](https://developers.google.com/apps-script/reference/spreadsheet/rich-text-value#getruns)関数を用いる。これの返り値は RichTextValue の配列となる。
この配列に格納されている値を一つ一つ HTML 化し、連結することでセルの内容を HTML にすることができる。

### TextStyle クラスから装飾情報の詳細を取得する

RichTextValue クラスの [getTextStyle](https://developers.google.com/apps-script/reference/spreadsheet/rich-text-value#gettextstyle) の帰り値は[TextStyle クラス](https://developers.google.com/apps-script/reference/spreadsheet/text-style)である。これを使うことで RichTextValue クラスが持つ装飾情報の詳細を取得できる。

このクラスが持つ以下の関数を用いて装飾情報を取得する。

* getFontFamily
* getFontSize
* getForegroundColorObject
* isBold
* isItalic
* isStrikethrough
* isUnderline

### 文字列のサニタイズ

上述の例 B1 のセルは `そう<strong>しょく</strong>なし` となっており、このまま出力すると「そう**しょく**なし」と出力される。しかし、これは「そう&lt;strong&gt;しょく&lt;/strong&gt;なし」と出力されてほしい。
今回は次のようにしてサニタイズした。

```javascript
const textTemplate = HtmlService.createTemplate('<?= text ?>');
textTemplate.text = "そう<strong>しょく</strong>なし";
const text = textTemplate.evaluate().getContent();
// text には そう&lt;strong&gt;しょく&lt;/strong&gt;なし が格納される
```

## 実際に書いたソースコード

### JavaScript

```javascript
function convertRunToHtml(run) {
  const textStyleMethods = {
    getFontFamily:            (result)=>{
      if(result) { decoration['font-family'] = result; }
    },
    getFontSize:              (result)=>{
      if(result) { decoration['font-size'] = `${result}px`; }
    },
    getForegroundColorObject: (result)=>{
      decoration['color'] = result.asRgbColor().asHexString();
    },
    isBold:                   (result)=>{
      if(result) { decoration['font-weight'] = 'bold'; }
    },
    isItalic:                 (result)=>{
      if(result) { decoration['font-style'] = 'italic'; }
    },
    isStrikethrough:          (result)=>{
      if(result) { decoration['text-decoration'] = decoration['text-decoration'] ? `${decoration['text-decoration']} line-through` : 'line-through';}
    },
    isUnderline:              (result)=>{
      if(result) { decoration['text-decoration'] = decoration['text-decoration'] ? `${decoration['text-decoration']} underline` : 'underline';}
    }
  };
  const decoration = {};
  const textStyle = run.getTextStyle();
  const link = run.getLinkUrl();
  const textTemplate = HtmlService.createTemplate('<?= text ?>');
  textTemplate.text = run.getText();
  const text = textTemplate.evaluate().getContent().replaceAll('\n', '<br/>');

  for(var method in textStyleMethods) {
    textStyleMethods[method](textStyle[method]());
  }
  const style = [];
  for(var key in decoration) {
    style.push(`${key}:${decoration[key]};`);
  }
  if(link) {
    return `<a href="${link}" style="${style.join('')}">${text}</a>`;
  } else {
    return `<span style="${style.join('')}">${text}</span>`;
  }
}

function convertCellToHtml(cell) {
  const result = [];
  const runs = cell.getRichTextValue().getRuns();
  for(var k = 0; k < runs.length; k++) {
    result.push(convertRunToHtml(runs[k]));
  }
  return `<td style="background-color:${cell.getBackground()};">${result.join('')}</td>`;
}

function getTable() {
  const range = SpreadsheetApp.getActiveSheet().getDataRange();
  const rawLength = range.getLastRow() + 1;
  const columnLength = range.getLastColumn() + 1;

  const result = [];
  for(var i = 1; i < rawLength; i++) {
    const rawResult = [];
    for(var j = 1; j < columnLength; j++) {
      rawResult.push(convertCellToHtml(range.getCell(i, j)))
    }
    result.push(rawResult);
  }
  return result;
}

function doGet() {
  return HtmlService.createTemplateFromFile('template.html').evaluate();
}
```

### HTML のテンプレート

```html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <table border="1" id="table">
      <? 
        const data = getTable();
        data.forEach((row)=>{
      ?>
        <tr>
          <? row.forEach((col)=>{ ?>
            <?!= col ?>
          <? }); ?>
        </tr>
      <? }); ?>
    </table>
  </body>
</html>
```

## 公開する

前回の記事の末尾に手順を書いているのでそちらで。

https://qiita.com/Chrysanthemum94/items/7bdaee4b1e0b7642dca2#http-%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B

# 得られた出力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633175/20ccb3e0-df5f-b1ec-a270-b73e6a2e7896.png)