選択中のテキストをコードブロックとして追加する macOS サービスの改修: Notion Tips (176)

はじめに

Notion Tips の第176回目は、3年前に作っていたらしい「選択中のテキストをコードブロックとして追加する macOSサービス」を、使いやすいように改修した話です。元々は、「Notion API を触ってみようの会」でのやり取りから始まっていました。

その中で、豚角煮チー牛さんが Save Snippet to Notion の話をしており、こんなポストをしていました。

その話を聞いて、そういえば私も昔作ったなと返信しました。

さらに調べると macOS のサービスも作っていたことが判明しました。この時の記事もはなてブログに掲載されています。

自分のサービスを確認してみたところ、そのまま残っていてショートカットキーを設定したらそのまま動いてしまいました。

ただ、内容を調べてみると3年前の API で Notion-Version も古いし、特定のデータベースの最新のページに貼る仕様だったので、今の使い方だと不便でした。今回は、以下の改修をすることにしました。

  1. Notion-Version を最新の 2022-06-28 に変更
  2. Database query endpoint ではなく、Search endpoint にしてインテグレーションがつながっている任意のページの最新のものに貼り付けるように変更

これによって、ユーザばインテグレーションキーを登録するだけになり、他の人にも勧めやすくなります。

作成方法

普通の人は以下のリポジトリに最新版のバイナリがあるので、ダウンロードしてインテグレーションキーを設定すれば完了です。

github.com

ここでは、同じようなサービスを作ってみたい人もいると思うので、作成方法を解説しておきます。今回はサービスを生成するために、macOS の Automator を使います。

Automation

起動すると何を作成するか聞かれるので、「クイックアクション」を追加します。

QuickAction

アクションは以下の二つを連結しています。

  1. JavaScript を実行
  2. Web ページを表示

Automation 画面

作成したスクリプト

基本は 1.0 の時とほぼ同様です。以下の変更をしています。

  1. getNotionPages の部分が https://api.notion.com/v1/search に変更
  2. 当時考慮していなかった 2000 文字を超える RichText を分割する処理を追加
  3. Notion-Version の更新に伴う text → rich_text の変更

特に 3 の変更はあまりに大昔すぎて、この変更があったことすら忘れていて時間がかかりました。NotionRubyMapping に JSON のお手本を書いてもらって判明しました。解決してよかったです。

// ########## Personal settings ##########
// Set your Notion API Token (strings that starts with `ntn_` or `secret_`)
const NOTION_API_TOKEN = "secret_XXXXXXXXXXXXXXXXXXXXXXXX"
// Set default language
const DEFAULT_LANGUAGE = "shell"
// If you want to open a new task by Notion.app, set true.  If you want to open it by your default browser, set false.
const OPEN_BY_APP = true
// Available languages
const languages = [
      "abap", "arduino", "bash", "basic", "c", "clojure", "coffeescript", "c++", "c#", "css", "dart", "diff", "docker",
      "elixir", "elm", "erlang", "flow", "fortran", "f#", "gherkin", "glsl", "go", "graphql", "groovy", "haskell", "html",
      "java", "javascript", "json", "julia", "kotlin", "latex", "less", "lisp", "livescript", "lua", "makefile", "markdown",
      "markup", "matlab", "mermaid", "nix", "objective-c", "ocaml", "pascal", "perl", "php", "plain text", "powershell",
      "prolog", "protobuf", "python", "r", "reason", "ruby", "rust", "sass", "scala", "scheme", "scss", "shell", "sql",
      "swift", "typescript", "vb.net", "verilog", "vhdl", "visual basic", "webassembly", "xml", "yaml", "java/c/c++/c#"
    ]

// show a dialog to register dateTimeStr
function dialogText(app, input) {
  const message = "Input language \n" +
    " Example: (no input) -> " + DEFAULT_LANGUAGE + "\n Available: " +
    languages.join(", ") + "\n for Message : \n" + input
  return app.displayDialog(message, {
    defaultAnswer: "",
    withIcon: "note",
    buttons: ["Cancel", "Continue"],
    defaultButton: "Continue"
  })
}

// Call Notion API
function sendNotion(app, url, payload, method) {
  const header = " --header 'Authorization: Bearer '" + NOTION_API_TOKEN + "'' "  +
    "--header 'Content-Type: application/json' " +
    "--header 'Notion-Version: 2022-06-28' " +
    "--data '"
  const script = "curl -X " + method + " " + url + header + payload.replaceAll("'", '\'"\'"\'') + "'"
  return JSON.parse(app.doShellScript(script))
}

function getNotionPages(app, payload) {
  const url = "https://api.notion.com/v1/search"
  return sendNotion(app, url, payload, "POST")
}

function getLastEditedPage(app) {
  const payload = {
    sort: {
      timestamp: "last_edited_time",
      direction: "descending"
    },
    page_size: 1
  }
  return getNotionPages(app, JSON.stringify(payload))
}

function appendBlockChildren(app, pageId, payload) {
  const url = "https://api.notion.com/v1/blocks/" + pageId + "/children"
  return sendNotion(app, url, payload, "PATCH")
}

function appendCodeBlock(app, page, code, language) {
  if (page) {
    const pageId = page.id
    let texts = []
    for (let i = 0; i < code.length; i+=1990) {
      texts.push({type: "text", text: {content: code.slice(i, i+1990)}})
    }
    const payload = {
      children: [
        {
          type: "code",
          object: "block",
          code: {
            rich_text: texts,
            language: language
          }
        }
      ]
    }
    const result = appendBlockChildren(app, pageId, JSON.stringify(payload))
    return result.url
  } else {
    return null
  }
}

function run(input, parameters) {
  const app = Application.currentApplication()
  app.includeStandardAdditions = true

  if (input.length > 0) {
    const inputStr = String(input)
    three_lines = inputStr.match("^[^\n]+\n[^\n]+\n[^\n]+") || inputStr
    const response = dialogText(app, three_lines)
    if (response.buttonReturned == "Continue") {
      var language = response.textReturned
      if (languages.indexOf(language) == -1) {
        language = "shell"
      }
      const lastEditedPage = getLastEditedPage(app).results[0]
      appendCodeBlock(app, lastEditedPage, inputStr, language)
      var url = lastEditedPage.url
      if (OPEN_BY_APP == true) {
        url = url.replace("https", "notion")
        let notion = Application("Notion")
        notion.activate()
      }
    }
    return url
  } else {
    return false
  }
}

利用方法

保存したら、キーボード - ショートカットのタグでショートカットを設定します。今回は、「Opt-Cmd-P」にしました。

サービスでショートカットを設定

コードブロックにしたいテキストを選択した状態で、設定したショートカットをタイプします。言語設定のダイアログが出るので、タイプします。デフォルトは shell になっています(スクリプト内で変更できます)。これによって、最後に編集したデータベースページの最後の部分にcode block が追加されます。

おわりに

今回の更新でタスクデータベースだけでなく、インテグレーションキーが結びついているレジュメのページなどにもコードを貼れるようになりました。すっかり忘れていましたが、これから便利に使っていきたいと思います。

hkob.notion.site