Skip to content

Instantly share code, notes, and snippets.

@agumonkey
Last active September 26, 2021 14:38
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save agumonkey/d3eee79840206c28ee9e50d8354a3fad to your computer and use it in GitHub Desktop.
Save agumonkey/d3eee79840206c28ee9e50d8354a3fad to your computer and use it in GitHub Desktop.
porting hackernews -> org-mode from js to elisp https://gist.github.com/dharmatech/2ef271907cb73298318fdb50a4ee7d54
;;; hn.el --- convert hacker news post into org-mode buffer -*- lexical-binding: t -*-
;; Copyright (C) 2018- Free Software Foundation, Inc.
;; Author: Johan Ponin <johan.ponin.pro@gmail.com>
;; Version: 0.0.1
;; Package-Version: 20181103.0001
;; Keywords: hackernews, org-mode
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Given an Hacker News story ID, generates an org-mode buffer of the comments
;;
;; @TODO: hn id caching (memoize hn/json)
;; @TODO: clean hidden http buffers
;; @TODO: unquote-html in text
;;
;; See documentation on <none>
;;
;; **Please note** The lexical binding in this file is not utilised at the
;; moment. We will take full advantage of lexical binding in an upcoming 3.0
;; release of Dash. In the meantime, we've added the pragma to avoid a bug that
;; you can read more about in https://github.com/magnars/dash.el/issues/130.
;;
;;; Code:
(require 'dash)
(require 'json)
(require 's)
(defvar *hn/ids-cache* '())
(defvar *hn/json-endpoint* "https://hacker-news.firebaseio.com/v0/item/%s.json?print=pretty")
;;; FP
(defun memoize (f) "Memoize F." :todo)
(defun alist-map (a k v c)
"Combinator to map over alist."
(-map (lambda (p) (funcall c
(funcall k (car p))
(funcall v (cdr p))))
a))
(defun walk (r n d c s)
"Generic walk combinator.
R: Root
N: function applied on a Node
D: function to go Down one level
C: function to combine N(n) with walk over childrens D(n)
S: state variable"
(funcall c (funcall n r s)
(let ((ns (1+ s)))
(-map (lambda (_) (walk _ n d c ns)) (funcall d r)))))
(defun walk! (node xform down io state)
"Generic walk effectful combinator.
NODE: node being walked
XFORM: function applied on NODE (with STATE)
DOWN: function to generate [child] form NODE
IO: effect function applied on XFORM(NODE, STATE)
STATE: state variable"
(let ((x (funcall xform node state)))
(funcall io x)
(let ((state (1+ state))
(children (funcall down node)))
(-each children (lambda (c) (walk! c xform down io state))))))
;;; ALIST
(defalias 'prop 'alist-get)
(defun props (o &rest ps)
"Helper to project an alist O on a list of keys PS.
ALIST -> ALIST"
(-zip ps (-map (lambda (p) (prop p o)) ps)))
;;; HTTP
(defun url-retrieve-synchronously:json (u)
"Fetch JSON from URL U."
(with-current-buffer (url-retrieve-synchronously u)
(goto-char url-http-end-of-headers)
(json-read)))
;;; HN
(defun hn/json (id)
"Convert HN ID to JSON.
ID: An Hacker news object ID"
(->> id
(format *hn/json-endpoint*)
(url-retrieve-synchronously:json)))
(defun hn/type (o)
"Accessor for type of O."
(prop 'type o))
(defun hn/kids (o)
"Hn object -> kids id list (from json response vector).
O: JSON Object"
(-map #'identity (cdr (assoc 'kids o))))
;;; FORMAT
(defun hn/render-text (s)
"Clean S by unquoting HTML."
(list :todo s))
(defun hn/format-properties (o s)
"ALIST a b -> ALIST str str -> str.
O: Alist
S: Stateful variable used for formatting"
(let* ((indent (indent s ? ))
(props-list (alist-map o
(lambda (k) (format "%s:%s:" indent (upcase (format "%S" k))))
(lambda (v) (format "%S" v))
(lambda (a b) (format "%s %s" a b))))
(props (mapconcat #'identity props-list "\n")))
(format "%s:PROPERTIES:\n%s\n%s:END:\n" indent props indent)))
(defun hn/format (o s)
"Format a JSON Object.
O: JSON Object
S: Stateful information"
(let ((k (hn/type o))
(indent (indent s ?*))
(author (prop 'by o))
(ps (hn/format-properties o s)))
(cond ((equal k "comment")
(let ((props (hn/format-properties o s))
(text (prop 'text o)))
(s-lex-format "${indent} ${author}\n${ps}\n\n ${text}")))
((equal k "story")
(let ((title (prop 'title o)))
(s-lex-format "#+TITLE ${title}\n#+AUTHOR ${author}\n${indent} ${title} by ${author}\n${ps}\n\n")))
(t (format "[unknown] %S \n" o)))))
;;; HELPERS
(defalias 'indent 'make-string)
;;; MAIN
;;; id -> json -> walk
(defun hn/to-org-mode! (id)
"Main function (effectful).
ID: Hacker News Story ID"
(let ((output (get-buffer-create (format "hn-%s" id))))
(walk! (hn/json id)
#'hn/format
(lambda (r) (-map #'hn/json (hn/kids r)))
(lambda (s)
(with-current-buffer output
(insert s)
(insert "\n")))
1)
(with-current-buffer output
(org-mode)
(switch-to-buffer-other-window output))))
(defun hn/hn (id)
"Main command.
ID: Hacker News Story ID"
(interactive "sid: ")
(message "> %s" id)
(hn/to-org-mode id))
;;; TEST
(defvar *hn-ids*
'("https://news.ycombinator.com/item?id=18363974"
"https://news.ycombinator.com/item?id=18370208"
"https://news.ycombinator.com/item?id=18360847"))
(defun hn/test! ()
"Test."
(interactive)
(hn/to-org-mode! "18360847"))
(provide 'hn)
;;; hn.el ends here
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment