/page.go

https://github.com/kjk/notionapi · Go · 294 lines · 226 code · 32 blank · 36 comment · 55 complexity · 2ce412e0da1241035825aba80f62fa88 MD5 · raw file

  1. package notionapi
  2. import (
  3. "errors"
  4. "fmt"
  5. "sort"
  6. )
  7. var (
  8. // TODO: add more values, see FormatPage struct
  9. validFormatValues = map[string]struct{}{
  10. "page_full_width": struct{}{},
  11. "page_small_text": struct{}{},
  12. }
  13. )
  14. // Page describes a single Notion page
  15. type Page struct {
  16. ID string
  17. // expose raw records for all data associated with this page
  18. BlockRecords []*Record
  19. UserRecords []*Record
  20. CollectionRecords []*Record
  21. CollectionViewRecords []*Record
  22. DiscussionRecords []*Record
  23. CommentRecords []*Record
  24. // for every block of type collection_view and its view_ids
  25. // we } TableView representing that collection view_id
  26. TableViews []*TableView
  27. idToBlock map[string]*Block
  28. idToUser map[string]*User
  29. idToCollection map[string]*Collection
  30. idToCollectionView map[string]*CollectionView
  31. idToComment map[string]*Comment
  32. idToDiscussion map[string]*Discussion
  33. blocksToSkip map[string]struct{} // not alive or when server doesn't return "value" for this block id
  34. client *Client
  35. }
  36. // BlockByID returns a block by its id
  37. func (p *Page) BlockByID(id string) *Block {
  38. return p.idToBlock[ToDashID(id)]
  39. }
  40. // UserByID returns a user by its id
  41. func (p *Page) UserByID(id string) *User {
  42. return p.idToUser[ToDashID(id)]
  43. }
  44. // CollectionByID returns a collection by its id
  45. func (p *Page) CollectionByID(id string) *Collection {
  46. return p.idToCollection[ToDashID(id)]
  47. }
  48. // CollectionViewByID returns a collection view by its id
  49. func (p *Page) CollectionViewByID(id string) *CollectionView {
  50. return p.idToCollectionView[ToDashID(id)]
  51. }
  52. // DiscussionByID returns a discussion by its id
  53. func (p *Page) DiscussionByID(id string) *Discussion {
  54. return p.idToDiscussion[ToDashID(id)]
  55. }
  56. // CommentByID returns a comment by its id
  57. func (p *Page) CommentByID(id string) *Comment {
  58. return p.idToComment[ToDashID(id)]
  59. }
  60. // Root returns a root block representing a page
  61. func (p *Page) Root() *Block {
  62. return p.BlockByID(p.ID)
  63. }
  64. // SetTitle changes page title
  65. func (p *Page) SetTitle(s string) error {
  66. op := p.Root().SetTitleOp(s)
  67. ops := []*Operation{op}
  68. return p.client.SubmitTransaction(ops)
  69. }
  70. // SetFormat changes format properties of a page. Valid values are:
  71. // page_full_width (bool), page_small_text (bool)
  72. func (p *Page) SetFormat(args map[string]interface{}) error {
  73. if len(args) == 0 {
  74. return errors.New("args can't be empty")
  75. }
  76. for k := range args {
  77. if _, ok := validFormatValues[k]; !ok {
  78. return fmt.Errorf("'%s' is not a valid page format property", k)
  79. }
  80. }
  81. op := p.Root().UpdateFormatOp(args)
  82. ops := []*Operation{op}
  83. return p.client.SubmitTransaction(ops)
  84. }
  85. // NotionURL returns url of this page on notion.so
  86. func (p *Page) NotionURL() string {
  87. if p == nil {
  88. return ""
  89. }
  90. id := ToNoDashID(p.ID)
  91. // TODO: maybe add title?
  92. return "https://www.notion.so/" + id
  93. }
  94. func forEachBlockWithParent(seen map[string]bool, blocks []*Block, parent *Block, cb func(*Block)) {
  95. for _, block := range blocks {
  96. id := block.ID
  97. if seen[id] {
  98. // crash rather than have infinite recursion
  99. panic("seen the same page again")
  100. }
  101. if parent != nil && (block.Type == BlockPage || block.Type == BlockCollectionViewPage) {
  102. // skip sub-pages to avoid infnite recursion
  103. continue
  104. }
  105. seen[id] = true
  106. block.Parent = parent
  107. cb(block)
  108. forEachBlockWithParent(seen, block.Content, block, cb)
  109. }
  110. }
  111. // ForEachBlock traverses the tree of blocks and calls cb on every block
  112. // in depth-first order. To traverse every blocks in a Page, do:
  113. // ForEachBlock([]*notionapi.Block{page.Root}, cb)
  114. func ForEachBlock(blocks []*Block, cb func(*Block)) {
  115. seen := map[string]bool{}
  116. forEachBlockWithParent(seen, blocks, nil, cb)
  117. }
  118. // ForEachBlock recursively calls cb for each block in th epage
  119. func (p *Page) ForEachBlock(cb func(*Block)) {
  120. seen := map[string]bool{}
  121. blocks := []*Block{p.Root()}
  122. forEachBlockWithParent(seen, blocks, nil, cb)
  123. }
  124. func panicIf(cond bool, args ...interface{}) {
  125. if !cond {
  126. return
  127. }
  128. if len(args) == 0 {
  129. panic("condition failed")
  130. }
  131. format := args[0].(string)
  132. if len(args) == 1 {
  133. panic(format)
  134. }
  135. panic(fmt.Sprintf(format, args[1:]))
  136. }
  137. // IsSubPage returns true if a given block is BlockPage and
  138. // a direct child of this page (as opposed to a link to
  139. // arbitrary page)
  140. func (p *Page) IsSubPage(block *Block) bool {
  141. if block == nil || !isPageBlock(block) {
  142. return false
  143. }
  144. for {
  145. parentID := block.ParentID
  146. if parentID == p.ID {
  147. return true
  148. }
  149. parent := p.BlockByID(block.ParentID)
  150. if parent == nil {
  151. return false
  152. }
  153. // parent is page but not our page, so it can't be sub-page
  154. if parent.Type == BlockPage {
  155. return false
  156. }
  157. block = parent
  158. }
  159. }
  160. // IsRoot returns true if this block is root block of the page
  161. // i.e. of type BlockPage and very first block
  162. func (p *Page) IsRoot(block *Block) bool {
  163. if block == nil || block.Type != BlockPage {
  164. return false
  165. }
  166. // a block can be a link to its parent, causing infinite loop
  167. // https://github.com/kjk/notionapi/issues/21
  168. // TODO: why block.ID == block.ParentID doesn't work?
  169. if block == block.Parent {
  170. return false
  171. }
  172. return block.ID == p.ID
  173. }
  174. func isPageBlock(block *Block) bool {
  175. switch block.Type {
  176. case BlockPage, BlockCollectionViewPage:
  177. return true
  178. }
  179. return false
  180. }
  181. // GetSubPages return list of ids for direct sub-pages of this page
  182. func (p *Page) GetSubPages() []string {
  183. root := p.Root()
  184. panicIf(!isPageBlock(root))
  185. subPages := map[string]struct{}{}
  186. seenBlocks := map[string]struct{}{}
  187. blocksToVisit := append([]string{}, root.ContentIDs...)
  188. for len(blocksToVisit) > 0 {
  189. id := ToDashID(blocksToVisit[0])
  190. blocksToVisit = blocksToVisit[1:]
  191. if _, ok := seenBlocks[id]; ok {
  192. continue
  193. }
  194. seenBlocks[id] = struct{}{}
  195. block := p.BlockByID(id)
  196. if p.IsSubPage(block) {
  197. subPages[id] = struct{}{}
  198. }
  199. // need to recursively scan blocks with children
  200. blocksToVisit = append(blocksToVisit, block.ContentIDs...)
  201. }
  202. res := []string{}
  203. for id := range subPages {
  204. res = append(res, id)
  205. }
  206. sort.Strings(res)
  207. return res
  208. }
  209. func makeUserName(user *User) string {
  210. s := user.GivenName
  211. if len(s) > 0 {
  212. s += " "
  213. }
  214. s += user.FamilyName
  215. if len(s) > 0 {
  216. return s
  217. }
  218. return user.ID
  219. }
  220. // GetUserNameByID returns a full user name given user id
  221. // it's a helper function
  222. func GetUserNameByID(page *Page, userID string) string {
  223. for _, r := range page.UserRecords {
  224. user := r.User
  225. if user.ID == userID {
  226. return makeUserName(user)
  227. }
  228. }
  229. return userID
  230. }
  231. func (p *Page) resolveBlocks() error {
  232. for _, block := range p.idToBlock {
  233. err := resolveBlock(p, block)
  234. if err != nil {
  235. return err
  236. }
  237. }
  238. return nil
  239. }
  240. func resolveBlock(p *Page, block *Block) error {
  241. if block.isResolved {
  242. return nil
  243. }
  244. block.isResolved = true
  245. err := parseProperties(block)
  246. if err != nil {
  247. return err
  248. }
  249. var contentIDs []string
  250. var content []*Block
  251. for _, id := range block.ContentIDs {
  252. b := p.idToBlock[id]
  253. if b == nil {
  254. continue
  255. }
  256. contentIDs = append(contentIDs, id)
  257. content = append(content, b)
  258. }
  259. block.ContentIDs = contentIDs
  260. block.Content = content
  261. return nil
  262. }